@intentius/chant 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +365 -0
- package/package.json +22 -0
- package/src/attrref.test.ts +148 -0
- package/src/attrref.ts +50 -0
- package/src/barrel.test.ts +157 -0
- package/src/barrel.ts +101 -0
- package/src/bench.test.ts +227 -0
- package/src/build.test.ts +437 -0
- package/src/build.ts +425 -0
- package/src/builder.test.ts +312 -0
- package/src/builder.ts +56 -0
- package/src/child-project.ts +44 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
- package/src/cli/commands/build.test.ts +149 -0
- package/src/cli/commands/build.ts +344 -0
- package/src/cli/commands/diff.test.ts +148 -0
- package/src/cli/commands/diff.ts +221 -0
- package/src/cli/commands/doctor.test.ts +239 -0
- package/src/cli/commands/doctor.ts +224 -0
- package/src/cli/commands/import.test.ts +379 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/init-lexicon.test.ts +297 -0
- package/src/cli/commands/init-lexicon.ts +993 -0
- package/src/cli/commands/init.test.ts +317 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/licenses.ts +165 -0
- package/src/cli/commands/lint.test.ts +332 -0
- package/src/cli/commands/lint.ts +408 -0
- package/src/cli/commands/list.test.ts +100 -0
- package/src/cli/commands/list.ts +108 -0
- package/src/cli/commands/update.test.ts +38 -0
- package/src/cli/commands/update.ts +207 -0
- package/src/cli/conflict-check.test.ts +255 -0
- package/src/cli/conflict-check.ts +89 -0
- package/src/cli/debug.ts +8 -0
- package/src/cli/format.test.ts +140 -0
- package/src/cli/format.ts +133 -0
- package/src/cli/handlers/build.ts +58 -0
- package/src/cli/handlers/dev.ts +38 -0
- package/src/cli/handlers/init.ts +46 -0
- package/src/cli/handlers/lint.ts +36 -0
- package/src/cli/handlers/misc.ts +57 -0
- package/src/cli/handlers/serve.ts +26 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/lsp/capabilities.ts +46 -0
- package/src/cli/lsp/diagnostics.ts +52 -0
- package/src/cli/lsp/server.test.ts +618 -0
- package/src/cli/lsp/server.ts +393 -0
- package/src/cli/main.test.ts +257 -0
- package/src/cli/main.ts +224 -0
- package/src/cli/mcp/resources/context.ts +59 -0
- package/src/cli/mcp/server.test.ts +747 -0
- package/src/cli/mcp/server.ts +402 -0
- package/src/cli/mcp/tools/build.ts +117 -0
- package/src/cli/mcp/tools/import.ts +48 -0
- package/src/cli/mcp/tools/lint.ts +45 -0
- package/src/cli/plugins.test.ts +31 -0
- package/src/cli/plugins.ts +94 -0
- package/src/cli/registry.ts +73 -0
- package/src/cli/reporters/stylish.test.ts +282 -0
- package/src/cli/reporters/stylish.ts +186 -0
- package/src/cli/watch.test.ts +81 -0
- package/src/cli/watch.ts +101 -0
- package/src/codegen/case.test.ts +30 -0
- package/src/codegen/case.ts +11 -0
- package/src/codegen/coverage.ts +167 -0
- package/src/codegen/docs.ts +634 -0
- package/src/codegen/fetch.test.ts +119 -0
- package/src/codegen/fetch.ts +261 -0
- package/src/codegen/generate-registry.test.ts +118 -0
- package/src/codegen/generate-registry.ts +107 -0
- package/src/codegen/generate-runtime-index.test.ts +81 -0
- package/src/codegen/generate-runtime-index.ts +99 -0
- package/src/codegen/generate-typescript.test.ts +146 -0
- package/src/codegen/generate-typescript.ts +161 -0
- package/src/codegen/generate.ts +206 -0
- package/src/codegen/json-patch.test.ts +113 -0
- package/src/codegen/json-patch.ts +151 -0
- package/src/codegen/json-schema.test.ts +196 -0
- package/src/codegen/json-schema.ts +209 -0
- package/src/codegen/naming.ts +201 -0
- package/src/codegen/package.ts +161 -0
- package/src/codegen/rollback.test.ts +92 -0
- package/src/codegen/rollback.ts +115 -0
- package/src/codegen/topo-sort.test.ts +69 -0
- package/src/codegen/topo-sort.ts +46 -0
- package/src/codegen/typecheck.test.ts +37 -0
- package/src/codegen/typecheck.ts +74 -0
- package/src/codegen/validate.test.ts +86 -0
- package/src/codegen/validate.ts +143 -0
- package/src/composite.test.ts +426 -0
- package/src/composite.ts +243 -0
- package/src/config.test.ts +91 -0
- package/src/config.ts +87 -0
- package/src/declarable.test.ts +160 -0
- package/src/declarable.ts +47 -0
- package/src/detectLexicon.test.ts +236 -0
- package/src/detectLexicon.ts +37 -0
- package/src/discovery/cache.test.ts +78 -0
- package/src/discovery/cache.ts +86 -0
- package/src/discovery/collect.test.ts +269 -0
- package/src/discovery/collect.ts +51 -0
- package/src/discovery/cycles.test.ts +238 -0
- package/src/discovery/cycles.ts +107 -0
- package/src/discovery/files.test.ts +154 -0
- package/src/discovery/files.ts +61 -0
- package/src/discovery/graph.test.ts +476 -0
- package/src/discovery/graph.ts +150 -0
- package/src/discovery/import.test.ts +199 -0
- package/src/discovery/import.ts +20 -0
- package/src/discovery/index.test.ts +272 -0
- package/src/discovery/index.ts +132 -0
- package/src/discovery/resolve.test.ts +267 -0
- package/src/discovery/resolve.ts +54 -0
- package/src/errors.test.ts +138 -0
- package/src/errors.ts +86 -0
- package/src/import/base-parser.test.ts +67 -0
- package/src/import/base-parser.ts +48 -0
- package/src/import/generator.ts +21 -0
- package/src/import/ir-utils.test.ts +103 -0
- package/src/import/ir-utils.ts +87 -0
- package/src/import/parser.ts +41 -0
- package/src/index.ts +60 -0
- package/src/intrinsic-interpolation.test.ts +91 -0
- package/src/intrinsic-interpolation.ts +89 -0
- package/src/intrinsic.test.ts +69 -0
- package/src/intrinsic.ts +43 -0
- package/src/lexicon-integrity.test.ts +94 -0
- package/src/lexicon-integrity.ts +69 -0
- package/src/lexicon-manifest.test.ts +101 -0
- package/src/lexicon-manifest.ts +71 -0
- package/src/lexicon-output.test.ts +182 -0
- package/src/lexicon-output.ts +82 -0
- package/src/lexicon-schema.test.ts +239 -0
- package/src/lexicon-schema.ts +144 -0
- package/src/lexicon.ts +212 -0
- package/src/lint/config-overrides.test.ts +254 -0
- package/src/lint/config.test.ts +644 -0
- package/src/lint/config.ts +375 -0
- package/src/lint/declarative.test.ts +256 -0
- package/src/lint/declarative.ts +187 -0
- package/src/lint/engine.test.ts +465 -0
- package/src/lint/engine.ts +172 -0
- package/src/lint/named-checks.test.ts +37 -0
- package/src/lint/named-checks.ts +33 -0
- package/src/lint/parser.test.ts +129 -0
- package/src/lint/parser.ts +42 -0
- package/src/lint/post-synth.test.ts +113 -0
- package/src/lint/post-synth.ts +76 -0
- package/src/lint/presets/relaxed.json +19 -0
- package/src/lint/presets/strict.json +19 -0
- package/src/lint/rule-loader.test.ts +67 -0
- package/src/lint/rule-loader.ts +67 -0
- package/src/lint/rule-options.test.ts +141 -0
- package/src/lint/rule.test.ts +196 -0
- package/src/lint/rule.ts +98 -0
- package/src/lint/rules/barrel-import-style.test.ts +80 -0
- package/src/lint/rules/barrel-import-style.ts +59 -0
- package/src/lint/rules/composite-scope.ts +55 -0
- package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
- package/src/lint/rules/cor017-composite-name-match.ts +108 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
- package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
- package/src/lint/rules/declarable-naming-convention.ts +70 -0
- package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
- package/src/lint/rules/enforce-barrel-import.ts +81 -0
- package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
- package/src/lint/rules/enforce-barrel-ref.ts +75 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
- package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
- package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
- package/src/lint/rules/evl004-spread-non-const.ts +111 -0
- package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
- package/src/lint/rules/evl005-resource-block-body.ts +49 -0
- package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
- package/src/lint/rules/evl006-barrel-usage.ts +95 -0
- package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
- package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
- package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
- package/src/lint/rules/export-required.test.ts +213 -0
- package/src/lint/rules/export-required.ts +158 -0
- package/src/lint/rules/file-declarable-limit.test.ts +148 -0
- package/src/lint/rules/file-declarable-limit.ts +96 -0
- package/src/lint/rules/flat-declarations.test.ts +210 -0
- package/src/lint/rules/flat-declarations.ts +70 -0
- package/src/lint/rules/index.ts +99 -0
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
- package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
- package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
- package/src/lint/rules/no-redundant-type-import.ts +85 -0
- package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
- package/src/lint/rules/no-redundant-value-cast.ts +46 -0
- package/src/lint/rules/no-string-ref.test.ts +100 -0
- package/src/lint/rules/no-string-ref.ts +66 -0
- package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
- package/src/lint/rules/no-unused-declarable-import.ts +103 -0
- package/src/lint/rules/no-unused-declarable.test.ts +134 -0
- package/src/lint/rules/no-unused-declarable.ts +118 -0
- package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
- package/src/lint/rules/prefer-namespace-import.ts +63 -0
- package/src/lint/rules/single-concern-file.test.ts +156 -0
- package/src/lint/rules/single-concern-file.ts +98 -0
- package/src/lint/rules/stale-barrel-types.ts +60 -0
- package/src/lint/selectors.test.ts +113 -0
- package/src/lint/selectors.ts +188 -0
- package/src/lsp/lexicon-providers.ts +191 -0
- package/src/lsp/types.ts +79 -0
- package/src/mcp/types.ts +22 -0
- package/src/project/scan.test.ts +178 -0
- package/src/project/scan.ts +182 -0
- package/src/project/sync.test.ts +87 -0
- package/src/project/sync.ts +46 -0
- package/src/project-validation.test.ts +64 -0
- package/src/project-validation.ts +79 -0
- package/src/pseudo-parameter.test.ts +39 -0
- package/src/pseudo-parameter.ts +47 -0
- package/src/runtime.ts +68 -0
- package/src/serializer-walker.test.ts +124 -0
- package/src/serializer-walker.ts +83 -0
- package/src/serializer.ts +42 -0
- package/src/sort.test.ts +290 -0
- package/src/sort.ts +58 -0
- package/src/stack-output.ts +82 -0
- package/src/types.test.ts +307 -0
- package/src/types.ts +46 -0
- package/src/utils.test.ts +195 -0
- package/src/utils.ts +46 -0
- package/src/validation.test.ts +308 -0
- package/src/validation.ts +50 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { buildCommand, type BuildOptions } from "./build";
|
|
3
|
+
import type { Serializer } from "../../serializer";
|
|
4
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
|
|
9
|
+
describe("buildCommand", () => {
|
|
10
|
+
let testDir: string;
|
|
11
|
+
let outputFile: string;
|
|
12
|
+
|
|
13
|
+
const mockSerializer: Serializer = {
|
|
14
|
+
name: "test",
|
|
15
|
+
rulePrefix: "TEST",
|
|
16
|
+
serialize: (entities) => {
|
|
17
|
+
const result: Record<string, unknown> = {};
|
|
18
|
+
for (const [name, entity] of entities) {
|
|
19
|
+
result[name] = { type: entity.entityType };
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify({ resources: result }, null, 2);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
testDir = join(tmpdir(), `chant-cli-test-${Date.now()}-${Math.random()}`);
|
|
27
|
+
outputFile = join(testDir, "output.json");
|
|
28
|
+
await mkdir(testDir, { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await rm(testDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("builds empty directory successfully", async () => {
|
|
36
|
+
const options: BuildOptions = {
|
|
37
|
+
path: testDir,
|
|
38
|
+
format: "json",
|
|
39
|
+
serializers: [mockSerializer],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const result = await buildCommand(options);
|
|
43
|
+
|
|
44
|
+
expect(result.success).toBe(true);
|
|
45
|
+
expect(result.errors).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("builds directory with entities", async () => {
|
|
49
|
+
// Create a test infrastructure file
|
|
50
|
+
const infraFile = join(testDir, "test.infra.ts");
|
|
51
|
+
await writeFile(
|
|
52
|
+
infraFile,
|
|
53
|
+
`
|
|
54
|
+
export const testEntity = {
|
|
55
|
+
lexicon: "test",
|
|
56
|
+
entityType: "TestEntity",
|
|
57
|
+
[Symbol.for("chant.declarable")]: true,
|
|
58
|
+
};
|
|
59
|
+
`
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const options: BuildOptions = {
|
|
63
|
+
path: testDir,
|
|
64
|
+
format: "json",
|
|
65
|
+
serializers: [mockSerializer],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = await buildCommand(options);
|
|
69
|
+
|
|
70
|
+
expect(result.success).toBe(true);
|
|
71
|
+
expect(result.resourceCount).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("writes output to file when specified", async () => {
|
|
75
|
+
const options: BuildOptions = {
|
|
76
|
+
path: testDir,
|
|
77
|
+
output: outputFile,
|
|
78
|
+
format: "json",
|
|
79
|
+
serializers: [mockSerializer],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = await buildCommand(options);
|
|
83
|
+
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
86
|
+
|
|
87
|
+
const content = readFileSync(outputFile, "utf-8");
|
|
88
|
+
expect(() => JSON.parse(content)).not.toThrow();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("returns errors for invalid files", async () => {
|
|
92
|
+
// Create a broken TypeScript file
|
|
93
|
+
const infraFile = join(testDir, "broken.infra.ts");
|
|
94
|
+
await writeFile(infraFile, "this is not valid typescript {{{");
|
|
95
|
+
|
|
96
|
+
const options: BuildOptions = {
|
|
97
|
+
path: testDir,
|
|
98
|
+
format: "json",
|
|
99
|
+
serializers: [mockSerializer],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = await buildCommand(options);
|
|
103
|
+
|
|
104
|
+
// Should still complete but may have errors
|
|
105
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("handles yaml format option", async () => {
|
|
109
|
+
const options: BuildOptions = {
|
|
110
|
+
path: testDir,
|
|
111
|
+
output: outputFile.replace(".json", ".yaml"),
|
|
112
|
+
format: "yaml",
|
|
113
|
+
serializers: [mockSerializer],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = await buildCommand(options);
|
|
117
|
+
|
|
118
|
+
expect(result.success).toBe(true);
|
|
119
|
+
expect(existsSync(outputFile.replace(".json", ".yaml"))).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("result includes resource and file counts", async () => {
|
|
123
|
+
const options: BuildOptions = {
|
|
124
|
+
path: testDir,
|
|
125
|
+
format: "json",
|
|
126
|
+
serializers: [mockSerializer],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = await buildCommand(options);
|
|
130
|
+
|
|
131
|
+
expect(result.resourceCount).toBeDefined();
|
|
132
|
+
expect(result.fileCount).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("handles invalid output path", async () => {
|
|
136
|
+
const options: BuildOptions = {
|
|
137
|
+
path: testDir,
|
|
138
|
+
output: "/nonexistent/directory/file.json",
|
|
139
|
+
format: "json",
|
|
140
|
+
serializers: [mockSerializer],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = await buildCommand(options);
|
|
144
|
+
|
|
145
|
+
// Should report error about output file
|
|
146
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
147
|
+
expect(result.errors.some((e) => e.includes("output"))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { build } from "../../build";
|
|
2
|
+
import type { Serializer, SerializerResult } from "../../serializer";
|
|
3
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
4
|
+
import { runPostSynthChecks } from "../../lint/post-synth";
|
|
5
|
+
import type { PostSynthCheck } from "../../lint/post-synth";
|
|
6
|
+
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
7
|
+
import { writeFileSync } from "fs";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build command options
|
|
13
|
+
*/
|
|
14
|
+
export interface BuildOptions {
|
|
15
|
+
/** Path to infrastructure directory */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Output file path (undefined = stdout) */
|
|
18
|
+
output?: string;
|
|
19
|
+
/** Output format */
|
|
20
|
+
format: "json" | "yaml";
|
|
21
|
+
/** Serializers to use for serialization */
|
|
22
|
+
serializers: Serializer[];
|
|
23
|
+
/** Lexicon plugins (for post-synth checks) */
|
|
24
|
+
plugins?: LexiconPlugin[];
|
|
25
|
+
/** Print summary to stderr */
|
|
26
|
+
verbose?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build command result
|
|
31
|
+
*/
|
|
32
|
+
export interface BuildResult {
|
|
33
|
+
/** Whether the build succeeded */
|
|
34
|
+
success: boolean;
|
|
35
|
+
/** Number of resources built */
|
|
36
|
+
resourceCount: number;
|
|
37
|
+
/** Number of source files processed */
|
|
38
|
+
fileCount: number;
|
|
39
|
+
/** Error messages */
|
|
40
|
+
errors: string[];
|
|
41
|
+
/** Warning messages */
|
|
42
|
+
warnings: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute the build command
|
|
47
|
+
*/
|
|
48
|
+
export async function buildCommand(options: BuildOptions): Promise<BuildResult> {
|
|
49
|
+
const errors: string[] = [];
|
|
50
|
+
const warnings: string[] = [];
|
|
51
|
+
|
|
52
|
+
// Resolve the path
|
|
53
|
+
const infraPath = resolve(options.path);
|
|
54
|
+
|
|
55
|
+
// Run the build
|
|
56
|
+
const result = await build(infraPath, options.serializers);
|
|
57
|
+
|
|
58
|
+
// Format errors
|
|
59
|
+
for (const error of result.errors) {
|
|
60
|
+
const formatted = formatError({
|
|
61
|
+
file: "file" in error ? (error as unknown as Record<string, unknown>).file as string | undefined : undefined,
|
|
62
|
+
line: "line" in error ? (error as unknown as Record<string, unknown>).line as number | undefined : undefined,
|
|
63
|
+
column: "column" in error ? (error as unknown as Record<string, unknown>).column as number | undefined : undefined,
|
|
64
|
+
message: error.message,
|
|
65
|
+
name: error.name,
|
|
66
|
+
});
|
|
67
|
+
errors.push(formatted);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Format warnings
|
|
71
|
+
for (const warning of result.warnings) {
|
|
72
|
+
warnings.push(formatWarning({ message: warning }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Run post-synth checks from plugins
|
|
76
|
+
if (result.errors.length === 0 && options.plugins) {
|
|
77
|
+
const postSynthChecks: PostSynthCheck[] = [];
|
|
78
|
+
for (const plugin of options.plugins) {
|
|
79
|
+
if (plugin.postSynthChecks) {
|
|
80
|
+
postSynthChecks.push(...plugin.postSynthChecks());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (postSynthChecks.length > 0) {
|
|
85
|
+
const postDiags = runPostSynthChecks(postSynthChecks, result);
|
|
86
|
+
for (const diag of postDiags) {
|
|
87
|
+
const prefix = diag.entity ? `[${diag.entity}] ` : "";
|
|
88
|
+
const lexiconSuffix = diag.lexicon ? ` (${diag.lexicon})` : "";
|
|
89
|
+
if (diag.severity === "error") {
|
|
90
|
+
errors.push(formatError({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
|
|
91
|
+
} else {
|
|
92
|
+
warnings.push(formatWarning({ message: `${prefix}${diag.message}${lexiconSuffix}` }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle output
|
|
99
|
+
if (result.errors.length === 0 && errors.length === 0) {
|
|
100
|
+
// Extract primary content and collect additional files from SerializerResult
|
|
101
|
+
const additionalFiles = new Map<string, string>();
|
|
102
|
+
|
|
103
|
+
function getPrimaryContent(raw: string | SerializerResult): string {
|
|
104
|
+
if (typeof raw === "string") return raw;
|
|
105
|
+
if (raw.files) {
|
|
106
|
+
for (const [filename, content] of Object.entries(raw.files)) {
|
|
107
|
+
additionalFiles.set(filename, content);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return raw.primary;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Try to parse content as JSON; return raw string if not JSON.
|
|
114
|
+
function tryParseJson(content: string): { json: unknown } | { raw: string } {
|
|
115
|
+
try {
|
|
116
|
+
return { json: JSON.parse(content) };
|
|
117
|
+
} catch {
|
|
118
|
+
return { raw: content };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Single lexicon: output the template directly
|
|
123
|
+
// Multiple lexicons: wrap in lexicon keys
|
|
124
|
+
let output: string = "{}";
|
|
125
|
+
if (result.outputs.size === 1) {
|
|
126
|
+
const [, raw] = [...result.outputs.entries()][0];
|
|
127
|
+
const content = getPrimaryContent(raw);
|
|
128
|
+
const parsed = tryParseJson(content);
|
|
129
|
+
if ("json" in parsed) {
|
|
130
|
+
output = JSON.stringify(parsed.json, sortedJsonReplacer, 2);
|
|
131
|
+
if (options.format === "yaml") {
|
|
132
|
+
output = jsonToYaml(JSON.parse(output));
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
output = parsed.raw;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// Multiple lexicons: JSON outputs get combined under lexicon keys,
|
|
139
|
+
// non-JSON outputs (e.g. YAML) are appended after a separator.
|
|
140
|
+
const combined: Record<string, unknown> = {};
|
|
141
|
+
const nonJsonSections: string[] = [];
|
|
142
|
+
const sortedLexiconNames = [...result.outputs.keys()].sort();
|
|
143
|
+
for (const lexiconName of sortedLexiconNames) {
|
|
144
|
+
const content = getPrimaryContent(result.outputs.get(lexiconName)!);
|
|
145
|
+
const parsed = tryParseJson(content);
|
|
146
|
+
if ("json" in parsed) {
|
|
147
|
+
combined[lexiconName] = parsed.json;
|
|
148
|
+
} else {
|
|
149
|
+
nonJsonSections.push(`# --- ${lexiconName} ---\n${parsed.raw}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const parts: string[] = [];
|
|
154
|
+
if (Object.keys(combined).length > 0) {
|
|
155
|
+
let jsonOutput = JSON.stringify(combined, sortedJsonReplacer, 2);
|
|
156
|
+
if (options.format === "yaml") {
|
|
157
|
+
jsonOutput = jsonToYaml(JSON.parse(jsonOutput));
|
|
158
|
+
}
|
|
159
|
+
parts.push(jsonOutput);
|
|
160
|
+
}
|
|
161
|
+
parts.push(...nonJsonSections);
|
|
162
|
+
if (parts.length > 0) {
|
|
163
|
+
output = parts.join("\n\n");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (options.output) {
|
|
168
|
+
// Write to file
|
|
169
|
+
try {
|
|
170
|
+
const outputPath = resolve(options.output);
|
|
171
|
+
writeFileSync(outputPath, output);
|
|
172
|
+
|
|
173
|
+
// Write additional files (e.g. nested stack templates) alongside the primary output
|
|
174
|
+
if (additionalFiles.size > 0) {
|
|
175
|
+
const { dirname, join } = require("path");
|
|
176
|
+
const outputDir = dirname(outputPath);
|
|
177
|
+
for (const [filename, content] of additionalFiles) {
|
|
178
|
+
let fileContent = content;
|
|
179
|
+
// Format additional files consistently
|
|
180
|
+
try {
|
|
181
|
+
const fileParsed = JSON.parse(content);
|
|
182
|
+
fileContent = JSON.stringify(fileParsed, sortedJsonReplacer, 2);
|
|
183
|
+
if (options.format === "yaml") {
|
|
184
|
+
fileContent = jsonToYaml(JSON.parse(fileContent));
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// If not JSON, write as-is
|
|
188
|
+
}
|
|
189
|
+
writeFileSync(join(outputDir, filename), fileContent);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
errors.push(
|
|
194
|
+
formatError({
|
|
195
|
+
message: `Failed to write output file: ${err instanceof Error ? err.message : String(err)}`,
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Print to stdout
|
|
201
|
+
console.log(output);
|
|
202
|
+
// Log additional files to stderr if any
|
|
203
|
+
for (const [filename, content] of additionalFiles) {
|
|
204
|
+
console.error(`\n--- ${filename} ---`);
|
|
205
|
+
console.error(content);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const resourceCount = result.entities.size;
|
|
211
|
+
const fileCount = result.sourceFileCount;
|
|
212
|
+
|
|
213
|
+
if (options.verbose && errors.length === 0) {
|
|
214
|
+
console.error(
|
|
215
|
+
formatSuccess(
|
|
216
|
+
`Built ${formatBold(String(resourceCount))} resources successfully`
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
success: errors.length === 0,
|
|
223
|
+
resourceCount,
|
|
224
|
+
fileCount,
|
|
225
|
+
errors,
|
|
226
|
+
warnings,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* JSON.stringify replacer that sorts object keys for deterministic output
|
|
232
|
+
*/
|
|
233
|
+
function sortedJsonReplacer(_key: string, value: unknown): unknown {
|
|
234
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
235
|
+
return Object.fromEntries(
|
|
236
|
+
Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b))
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Simple JSON to YAML converter
|
|
244
|
+
*/
|
|
245
|
+
function jsonToYaml(obj: unknown, indent = 0): string {
|
|
246
|
+
const spaces = " ".repeat(indent);
|
|
247
|
+
|
|
248
|
+
if (obj === null) return "null";
|
|
249
|
+
if (obj === undefined) return "~";
|
|
250
|
+
if (typeof obj === "boolean") return obj ? "true" : "false";
|
|
251
|
+
if (typeof obj === "number") return String(obj);
|
|
252
|
+
if (typeof obj === "string") {
|
|
253
|
+
// Quote strings that need it
|
|
254
|
+
if (obj.includes("\n") || obj.includes(":") || obj.includes("#")) {
|
|
255
|
+
return `"${obj.replace(/"/g, '\\"')}"`;
|
|
256
|
+
}
|
|
257
|
+
return obj;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (Array.isArray(obj)) {
|
|
261
|
+
if (obj.length === 0) return "[]";
|
|
262
|
+
return obj
|
|
263
|
+
.map((item) => `${spaces}- ${jsonToYaml(item, indent + 1).trimStart()}`)
|
|
264
|
+
.join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof obj === "object") {
|
|
268
|
+
const entries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b));
|
|
269
|
+
if (entries.length === 0) return "{}";
|
|
270
|
+
return entries
|
|
271
|
+
.map(([key, value]) => {
|
|
272
|
+
const yamlValue = jsonToYaml(value, indent + 1);
|
|
273
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
274
|
+
return `${spaces}${key}:\n${yamlValue}`;
|
|
275
|
+
}
|
|
276
|
+
return `${spaces}${key}: ${yamlValue.trimStart()}`;
|
|
277
|
+
})
|
|
278
|
+
.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return String(obj);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Print errors to stderr
|
|
286
|
+
*/
|
|
287
|
+
export function printErrors(errors: string[]): void {
|
|
288
|
+
for (const error of errors) {
|
|
289
|
+
console.error(error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Print warnings to stderr
|
|
295
|
+
*/
|
|
296
|
+
export function printWarnings(warnings: string[]): void {
|
|
297
|
+
for (const warning of warnings) {
|
|
298
|
+
console.error(warning);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Run build in watch mode. Runs an initial build, then watches for changes
|
|
304
|
+
* and triggers rebuilds. Returns a cleanup function.
|
|
305
|
+
*/
|
|
306
|
+
export function buildCommandWatch(
|
|
307
|
+
options: BuildOptions,
|
|
308
|
+
onRebuild?: (result: BuildResult) => void,
|
|
309
|
+
): () => void {
|
|
310
|
+
const infraPath = resolve(options.path);
|
|
311
|
+
|
|
312
|
+
console.error(formatInfo(`[${formatTimestamp()}] Watching for changes...`));
|
|
313
|
+
|
|
314
|
+
// Run initial build
|
|
315
|
+
buildCommand(options).then((result) => {
|
|
316
|
+
printWarnings(result.warnings);
|
|
317
|
+
printErrors(result.errors);
|
|
318
|
+
onRebuild?.(result);
|
|
319
|
+
console.error(formatInfo(`[${formatTimestamp()}] Waiting for changes...`));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Watch for changes and trigger rebuilds
|
|
323
|
+
const cleanup = watchDirectory(infraPath, async (changedFiles) => {
|
|
324
|
+
console.error("");
|
|
325
|
+
console.error(
|
|
326
|
+
formatInfo(
|
|
327
|
+
`[${formatTimestamp()}] Changes detected: ${formatChangedFiles(changedFiles, infraPath)}`,
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const result = await buildCommand(options);
|
|
333
|
+
printWarnings(result.warnings);
|
|
334
|
+
printErrors(result.errors);
|
|
335
|
+
onRebuild?.(result);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.error(formatInfo(`[${formatTimestamp()}] Waiting for changes...`));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return cleanup;
|
|
344
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { diffCommand, type DiffOptions } from "./diff";
|
|
3
|
+
import type { Serializer } from "../../serializer";
|
|
4
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
describe("diffCommand", () => {
|
|
9
|
+
let testDir: string;
|
|
10
|
+
|
|
11
|
+
const mockSerializer: Serializer = {
|
|
12
|
+
name: "test",
|
|
13
|
+
rulePrefix: "TEST",
|
|
14
|
+
serialize: (entities) => {
|
|
15
|
+
const result: Record<string, unknown> = {};
|
|
16
|
+
for (const [name, entity] of entities) {
|
|
17
|
+
result[name] = { type: entity.entityType };
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify({ resources: result }, null, 2);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
testDir = join(tmpdir(), `chant-diff-test-${Date.now()}-${Math.random()}`);
|
|
25
|
+
await mkdir(testDir, { recursive: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await rm(testDir, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("shows no changes for empty directory with no previous output", async () => {
|
|
33
|
+
const options: DiffOptions = {
|
|
34
|
+
path: testDir,
|
|
35
|
+
serializers: [mockSerializer],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = await diffCommand(options);
|
|
39
|
+
|
|
40
|
+
expect(result.success).toBe(true);
|
|
41
|
+
// Empty build vs empty previous — no meaningful changes beyond the empty object
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("shows additions when no previous output file exists", async () => {
|
|
45
|
+
const infraFile = join(testDir, "test.infra.ts");
|
|
46
|
+
await writeFile(
|
|
47
|
+
infraFile,
|
|
48
|
+
`
|
|
49
|
+
export const myBucket = {
|
|
50
|
+
lexicon: "test",
|
|
51
|
+
entityType: "TestBucket",
|
|
52
|
+
[Symbol.for("chant.declarable")]: true,
|
|
53
|
+
};
|
|
54
|
+
`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const options: DiffOptions = {
|
|
58
|
+
path: testDir,
|
|
59
|
+
output: join(testDir, "nonexistent.json"),
|
|
60
|
+
serializers: [mockSerializer],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result = await diffCommand(options);
|
|
64
|
+
|
|
65
|
+
expect(result.success).toBe(true);
|
|
66
|
+
expect(result.hasChanges).toBe(true);
|
|
67
|
+
expect(result.diff).toContain("+");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("shows no changes when output matches", async () => {
|
|
71
|
+
const infraFile = join(testDir, "test.infra.ts");
|
|
72
|
+
await writeFile(
|
|
73
|
+
infraFile,
|
|
74
|
+
`
|
|
75
|
+
export const myBucket = {
|
|
76
|
+
lexicon: "test",
|
|
77
|
+
entityType: "TestBucket",
|
|
78
|
+
[Symbol.for("chant.declarable")]: true,
|
|
79
|
+
};
|
|
80
|
+
`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// First, build to get the expected output
|
|
84
|
+
const { build } = await import("@intentius/chant/build");
|
|
85
|
+
const buildResult = await build(join(testDir), [mockSerializer]);
|
|
86
|
+
const combined: Record<string, unknown> = {};
|
|
87
|
+
const sortedSerializerNames = [...buildResult.outputs.keys()].sort();
|
|
88
|
+
for (const serializerName of sortedSerializerNames) {
|
|
89
|
+
combined[serializerName] = JSON.parse(buildResult.outputs.get(serializerName)!);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Sort keys to match diffCommand behavior
|
|
93
|
+
const sortedReplacer = (_key: string, value: unknown): unknown => {
|
|
94
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
95
|
+
return Object.fromEntries(
|
|
96
|
+
Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
101
|
+
const expectedOutput = JSON.stringify(combined, sortedReplacer, 2);
|
|
102
|
+
|
|
103
|
+
const outputFile = join(testDir, "output.json");
|
|
104
|
+
await writeFile(outputFile, expectedOutput);
|
|
105
|
+
|
|
106
|
+
const options: DiffOptions = {
|
|
107
|
+
path: testDir,
|
|
108
|
+
output: outputFile,
|
|
109
|
+
serializers: [mockSerializer],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = await diffCommand(options);
|
|
113
|
+
|
|
114
|
+
expect(result.success).toBe(true);
|
|
115
|
+
expect(result.hasChanges).toBe(false);
|
|
116
|
+
expect(result.diff).toBe("");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("shows diff when output differs", async () => {
|
|
120
|
+
const infraFile = join(testDir, "test.infra.ts");
|
|
121
|
+
await writeFile(
|
|
122
|
+
infraFile,
|
|
123
|
+
`
|
|
124
|
+
export const myBucket = {
|
|
125
|
+
lexicon: "test",
|
|
126
|
+
entityType: "TestBucket",
|
|
127
|
+
[Symbol.for("chant.declarable")]: true,
|
|
128
|
+
};
|
|
129
|
+
`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const outputFile = join(testDir, "output.json");
|
|
133
|
+
await writeFile(outputFile, '{\n "test": {\n "resources": {}\n }\n}');
|
|
134
|
+
|
|
135
|
+
const options: DiffOptions = {
|
|
136
|
+
path: testDir,
|
|
137
|
+
output: outputFile,
|
|
138
|
+
serializers: [mockSerializer],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = await diffCommand(options);
|
|
142
|
+
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
expect(result.hasChanges).toBe(true);
|
|
145
|
+
expect(result.diff).toContain("---");
|
|
146
|
+
expect(result.diff).toContain("+++");
|
|
147
|
+
});
|
|
148
|
+
});
|