@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,124 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { walkValue, type SerializerVisitor } from "./serializer-walker";
|
|
3
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
4
|
+
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
5
|
+
import { AttrRef } from "./attrref";
|
|
6
|
+
|
|
7
|
+
function makeDeclarable(type: string, kind: "resource" | "property" = "resource", props?: Record<string, unknown>): Declarable & { props?: Record<string, unknown> } {
|
|
8
|
+
const d: Declarable & { props?: Record<string, unknown> } = {
|
|
9
|
+
lexicon: "test",
|
|
10
|
+
entityType: type,
|
|
11
|
+
kind,
|
|
12
|
+
[DECLARABLE_MARKER]: true as const,
|
|
13
|
+
};
|
|
14
|
+
if (props) d.props = props;
|
|
15
|
+
return d;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const mockVisitor: SerializerVisitor = {
|
|
19
|
+
attrRef: (name, attr) => ({ __getAtt: [name, attr] }),
|
|
20
|
+
resourceRef: (name) => ({ __ref: name }),
|
|
21
|
+
propertyDeclarable: (entity, walk) => {
|
|
22
|
+
if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const props = entity.props as Record<string, unknown>;
|
|
26
|
+
const result: Record<string, unknown> = {};
|
|
27
|
+
for (const [k, v] of Object.entries(props)) {
|
|
28
|
+
if (v !== undefined) result[k] = walk(v);
|
|
29
|
+
}
|
|
30
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("walkValue", () => {
|
|
35
|
+
test("returns null and undefined as-is", () => {
|
|
36
|
+
const names = new Map<Declarable, string>();
|
|
37
|
+
expect(walkValue(null, names, mockVisitor)).toBe(null);
|
|
38
|
+
expect(walkValue(undefined, names, mockVisitor)).toBe(undefined);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns primitives as-is", () => {
|
|
42
|
+
const names = new Map<Declarable, string>();
|
|
43
|
+
expect(walkValue(42, names, mockVisitor)).toBe(42);
|
|
44
|
+
expect(walkValue("hello", names, mockVisitor)).toBe("hello");
|
|
45
|
+
expect(walkValue(true, names, mockVisitor)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("handles AttrRef", () => {
|
|
49
|
+
const parent = makeDeclarable("Test::Resource");
|
|
50
|
+
const ref = new AttrRef(parent, "arn");
|
|
51
|
+
ref._setLogicalName("MyResource");
|
|
52
|
+
|
|
53
|
+
const names = new Map<Declarable, string>([[parent, "MyResource"]]);
|
|
54
|
+
expect(walkValue(ref, names, mockVisitor)).toEqual({ __getAtt: ["MyResource", "arn"] });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("throws for AttrRef without logical name", () => {
|
|
58
|
+
const parent = makeDeclarable("Test::Resource");
|
|
59
|
+
const ref = new AttrRef(parent, "arn");
|
|
60
|
+
|
|
61
|
+
const names = new Map<Declarable, string>();
|
|
62
|
+
expect(() => walkValue(ref, names, mockVisitor)).toThrow("logical name not set");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("handles intrinsic with toJSON", () => {
|
|
66
|
+
const intrinsic = {
|
|
67
|
+
[INTRINSIC_MARKER]: true as const,
|
|
68
|
+
toJSON: () => ({ MyIntrinsic: "value" }),
|
|
69
|
+
};
|
|
70
|
+
const names = new Map<Declarable, string>();
|
|
71
|
+
expect(walkValue(intrinsic, names, mockVisitor)).toEqual({ MyIntrinsic: "value" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("handles resource Declarable via resourceRef", () => {
|
|
75
|
+
const resource = makeDeclarable("Test::Bucket");
|
|
76
|
+
const names = new Map<Declarable, string>([[resource, "MyBucket"]]);
|
|
77
|
+
expect(walkValue(resource, names, mockVisitor)).toEqual({ __ref: "MyBucket" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("handles property Declarable via propertyDeclarable", () => {
|
|
81
|
+
const prop = makeDeclarable("Test::Config", "property", { key: "value" });
|
|
82
|
+
const names = new Map<Declarable, string>();
|
|
83
|
+
expect(walkValue(prop, names, mockVisitor)).toEqual({ key: "value" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("recurses into arrays", () => {
|
|
87
|
+
const names = new Map<Declarable, string>();
|
|
88
|
+
expect(walkValue([1, "two", [3]], names, mockVisitor)).toEqual([1, "two", [3]]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("recurses into objects", () => {
|
|
92
|
+
const names = new Map<Declarable, string>();
|
|
93
|
+
expect(walkValue({ a: 1, b: { c: 2 } }, names, mockVisitor)).toEqual({ a: 1, b: { c: 2 } });
|
|
94
|
+
});
|
|
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
|
+
test("complex nested structure", () => {
|
|
106
|
+
const resource = makeDeclarable("Test::Role");
|
|
107
|
+
const ref = new AttrRef(resource, "arn");
|
|
108
|
+
ref._setLogicalName("MyRole");
|
|
109
|
+
|
|
110
|
+
const names = new Map<Declarable, string>([[resource, "MyRole"]]);
|
|
111
|
+
const value = {
|
|
112
|
+
config: {
|
|
113
|
+
role: resource,
|
|
114
|
+
items: [ref, "static"],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
expect(walkValue(value, names, mockVisitor)).toEqual({
|
|
118
|
+
config: {
|
|
119
|
+
role: { __ref: "MyRole" },
|
|
120
|
+
items: [{ __getAtt: ["MyRole", "arn"] }, "static"],
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic recursive value walker for lexicon serializers.
|
|
3
|
+
*
|
|
4
|
+
* Implements the dispatch chain: null → AttrRef → Intrinsic → Declarable → Array → Object,
|
|
5
|
+
* delegating format-specific behavior to a SerializerVisitor.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Declarable } from "./declarable";
|
|
9
|
+
import { isPropertyDeclarable } from "./declarable";
|
|
10
|
+
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
11
|
+
import { AttrRef } from "./attrref";
|
|
12
|
+
|
|
13
|
+
export interface SerializerVisitor {
|
|
14
|
+
/** Format an attribute reference (e.g. CFN Fn::GetAttr). */
|
|
15
|
+
attrRef(logicalName: string, attribute: string): unknown;
|
|
16
|
+
/** Format a resource-level Declarable reference (e.g. CFN Ref). */
|
|
17
|
+
resourceRef(logicalName: string): unknown;
|
|
18
|
+
/** Format a property-level Declarable by walking its props. */
|
|
19
|
+
propertyDeclarable(entity: Declarable, walk: (v: unknown) => unknown): unknown;
|
|
20
|
+
/** Optional key transformation (e.g. camelCase → PascalCase). */
|
|
21
|
+
transformKey?(key: string): string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recursively walk a value, converting AttrRefs, Intrinsics, Declarables,
|
|
26
|
+
* arrays, and objects using the provided visitor.
|
|
27
|
+
*/
|
|
28
|
+
export function walkValue(
|
|
29
|
+
value: unknown,
|
|
30
|
+
entityNames: Map<Declarable, string>,
|
|
31
|
+
visitor: SerializerVisitor,
|
|
32
|
+
): unknown {
|
|
33
|
+
if (value === null || value === undefined) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle AttrRef
|
|
38
|
+
if (value instanceof AttrRef) {
|
|
39
|
+
const name = value.getLogicalName();
|
|
40
|
+
if (!name) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Cannot serialize AttrRef for attribute "${value.attribute}": logical name not set`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return visitor.attrRef(name, value.attribute);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Handle Intrinsics
|
|
49
|
+
if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
|
|
50
|
+
if ("toJSON" in value && typeof value.toJSON === "function") {
|
|
51
|
+
return value.toJSON();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle Declarable references
|
|
56
|
+
if (typeof value === "object" && value !== null && "entityType" in value) {
|
|
57
|
+
const decl = value as Declarable;
|
|
58
|
+
if (isPropertyDeclarable(decl)) {
|
|
59
|
+
return visitor.propertyDeclarable(decl, (v) => walkValue(v, entityNames, visitor));
|
|
60
|
+
}
|
|
61
|
+
const name = entityNames.get(decl);
|
|
62
|
+
if (name) {
|
|
63
|
+
return visitor.resourceRef(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle arrays
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map((item) => walkValue(item, entityNames, visitor));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle objects
|
|
73
|
+
if (typeof value === "object") {
|
|
74
|
+
const result: Record<string, unknown> = {};
|
|
75
|
+
for (const [key, val] of Object.entries(value)) {
|
|
76
|
+
const outKey = visitor.transformKey ? visitor.transformKey(key) : key;
|
|
77
|
+
result[outKey] = walkValue(val, entityNames, visitor);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Declarable } from "./declarable";
|
|
2
|
+
import type { LexiconOutput } from "./lexicon-output";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Result of serialization that may include additional files (e.g. nested stack templates).
|
|
6
|
+
*/
|
|
7
|
+
export interface SerializerResult {
|
|
8
|
+
/** Primary template content */
|
|
9
|
+
primary: string;
|
|
10
|
+
/** Additional files keyed by filename (e.g. "network.template.json" → content) */
|
|
11
|
+
files?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Serializer interface for chant specifications
|
|
16
|
+
*/
|
|
17
|
+
export interface Serializer {
|
|
18
|
+
/**
|
|
19
|
+
* Name of the lexicon
|
|
20
|
+
*/
|
|
21
|
+
name: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Prefix used for rules in this lexicon
|
|
25
|
+
*/
|
|
26
|
+
rulePrefix: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Serializes the entities to a string representation
|
|
30
|
+
* @param entities - Map of entity name to Declarable entity
|
|
31
|
+
* @param outputs - Optional array of LexiconOutputs produced by this lexicon
|
|
32
|
+
*/
|
|
33
|
+
serialize(entities: Map<string, Declarable>, outputs?: LexiconOutput[]): string | SerializerResult;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Serialize a cross-lexicon reference to a foreign output.
|
|
37
|
+
* Called when this lexicon consumes an output produced by another lexicon.
|
|
38
|
+
* @param output - The LexiconOutput being referenced
|
|
39
|
+
* @returns Lexicon-specific reference representation
|
|
40
|
+
*/
|
|
41
|
+
serializeCrossRef?(output: LexiconOutput): unknown;
|
|
42
|
+
}
|
package/src/sort.test.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { topologicalSort } from "./sort";
|
|
3
|
+
import { BuildError } from "./errors";
|
|
4
|
+
|
|
5
|
+
describe("topologicalSort", () => {
|
|
6
|
+
test("returns empty array for empty dependencies", () => {
|
|
7
|
+
const dependencies = {};
|
|
8
|
+
const result = topologicalSort(dependencies);
|
|
9
|
+
expect(result).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("returns single node with no dependencies", () => {
|
|
13
|
+
const dependencies = {
|
|
14
|
+
A: [],
|
|
15
|
+
};
|
|
16
|
+
const result = topologicalSort(dependencies);
|
|
17
|
+
expect(result).toEqual(["A"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("sorts linear dependency chain", () => {
|
|
21
|
+
const dependencies = {
|
|
22
|
+
A: [],
|
|
23
|
+
B: ["A"],
|
|
24
|
+
C: ["B"],
|
|
25
|
+
D: ["C"],
|
|
26
|
+
};
|
|
27
|
+
const result = topologicalSort(dependencies);
|
|
28
|
+
expect(result).toEqual(["A", "B", "C", "D"]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("sorts diamond dependency pattern", () => {
|
|
32
|
+
const dependencies = {
|
|
33
|
+
A: [],
|
|
34
|
+
B: ["A"],
|
|
35
|
+
C: ["A"],
|
|
36
|
+
D: ["B", "C"],
|
|
37
|
+
};
|
|
38
|
+
const result = topologicalSort(dependencies);
|
|
39
|
+
|
|
40
|
+
// A must come first, D must come last
|
|
41
|
+
expect(result[0]).toBe("A");
|
|
42
|
+
expect(result[3]).toBe("D");
|
|
43
|
+
|
|
44
|
+
// B and C must come before D but after A
|
|
45
|
+
const bIndex = result.indexOf("B");
|
|
46
|
+
const cIndex = result.indexOf("C");
|
|
47
|
+
const dIndex = result.indexOf("D");
|
|
48
|
+
expect(bIndex).toBeLessThan(dIndex);
|
|
49
|
+
expect(cIndex).toBeLessThan(dIndex);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("sorts multiple independent nodes", () => {
|
|
53
|
+
const dependencies = {
|
|
54
|
+
A: [],
|
|
55
|
+
B: [],
|
|
56
|
+
C: [],
|
|
57
|
+
};
|
|
58
|
+
const result = topologicalSort(dependencies);
|
|
59
|
+
expect(result).toHaveLength(3);
|
|
60
|
+
expect(result).toContain("A");
|
|
61
|
+
expect(result).toContain("B");
|
|
62
|
+
expect(result).toContain("C");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("sorts complex graph with multiple levels", () => {
|
|
66
|
+
const dependencies = {
|
|
67
|
+
A: [],
|
|
68
|
+
B: [],
|
|
69
|
+
C: ["A"],
|
|
70
|
+
D: ["A", "B"],
|
|
71
|
+
E: ["C", "D"],
|
|
72
|
+
};
|
|
73
|
+
const result = topologicalSort(dependencies);
|
|
74
|
+
|
|
75
|
+
// A and B must come before C, D, E
|
|
76
|
+
const aIndex = result.indexOf("A");
|
|
77
|
+
const bIndex = result.indexOf("B");
|
|
78
|
+
const cIndex = result.indexOf("C");
|
|
79
|
+
const dIndex = result.indexOf("D");
|
|
80
|
+
const eIndex = result.indexOf("E");
|
|
81
|
+
|
|
82
|
+
expect(aIndex).toBeLessThan(cIndex);
|
|
83
|
+
expect(aIndex).toBeLessThan(dIndex);
|
|
84
|
+
expect(aIndex).toBeLessThan(eIndex);
|
|
85
|
+
expect(bIndex).toBeLessThan(dIndex);
|
|
86
|
+
expect(bIndex).toBeLessThan(eIndex);
|
|
87
|
+
expect(cIndex).toBeLessThan(eIndex);
|
|
88
|
+
expect(dIndex).toBeLessThan(eIndex);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("handles node with multiple dependencies", () => {
|
|
92
|
+
const dependencies = {
|
|
93
|
+
A: [],
|
|
94
|
+
B: [],
|
|
95
|
+
C: [],
|
|
96
|
+
D: ["A", "B", "C"],
|
|
97
|
+
};
|
|
98
|
+
const result = topologicalSort(dependencies);
|
|
99
|
+
|
|
100
|
+
// A, B, C must all come before D
|
|
101
|
+
const dIndex = result.indexOf("D");
|
|
102
|
+
expect(result.indexOf("A")).toBeLessThan(dIndex);
|
|
103
|
+
expect(result.indexOf("B")).toBeLessThan(dIndex);
|
|
104
|
+
expect(result.indexOf("C")).toBeLessThan(dIndex);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("throws BuildError for self-loop", () => {
|
|
108
|
+
const dependencies = {
|
|
109
|
+
A: ["A"],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
expect(() => topologicalSort(dependencies)).toThrow(BuildError);
|
|
113
|
+
expect(() => topologicalSort(dependencies)).toThrow(/Circular dependency detected/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("throws BuildError for two-node cycle", () => {
|
|
117
|
+
const dependencies = {
|
|
118
|
+
A: ["B"],
|
|
119
|
+
B: ["A"],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
expect(() => topologicalSort(dependencies)).toThrow(BuildError);
|
|
123
|
+
expect(() => topologicalSort(dependencies)).toThrow(/Circular dependency detected/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("throws BuildError for three-node cycle", () => {
|
|
127
|
+
const dependencies = {
|
|
128
|
+
A: ["B"],
|
|
129
|
+
B: ["C"],
|
|
130
|
+
C: ["A"],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
expect(() => topologicalSort(dependencies)).toThrow(BuildError);
|
|
134
|
+
expect(() => topologicalSort(dependencies)).toThrow(/Circular dependency detected/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("throws BuildError for cycle in complex graph", () => {
|
|
138
|
+
const dependencies = {
|
|
139
|
+
A: [],
|
|
140
|
+
B: ["A"],
|
|
141
|
+
C: ["B"],
|
|
142
|
+
D: ["C"],
|
|
143
|
+
E: ["D", "B"],
|
|
144
|
+
F: ["E"],
|
|
145
|
+
G: ["F", "C"],
|
|
146
|
+
// Create cycle: G -> C -> B, but also E -> B
|
|
147
|
+
H: ["G"],
|
|
148
|
+
I: ["H", "A"],
|
|
149
|
+
J: ["I"],
|
|
150
|
+
// Add the cycle
|
|
151
|
+
K: ["J"],
|
|
152
|
+
L: ["K"],
|
|
153
|
+
M: ["L", "K"],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Add actual cycle
|
|
157
|
+
dependencies.B = ["M"];
|
|
158
|
+
|
|
159
|
+
expect(() => topologicalSort(dependencies)).toThrow(BuildError);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("BuildError contains entity name from cycle", () => {
|
|
163
|
+
const dependencies = {
|
|
164
|
+
A: ["B"],
|
|
165
|
+
B: ["C"],
|
|
166
|
+
C: ["A"],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
topologicalSort(dependencies);
|
|
171
|
+
expect(true).toBe(false); // Should not reach here
|
|
172
|
+
} catch (error) {
|
|
173
|
+
expect(error).toBeInstanceOf(BuildError);
|
|
174
|
+
if (error instanceof BuildError) {
|
|
175
|
+
// Entity name should be one of the nodes in the cycle
|
|
176
|
+
expect(["A", "B", "C"]).toContain(error.entityName);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("BuildError message includes cycle path", () => {
|
|
182
|
+
const dependencies = {
|
|
183
|
+
A: ["B"],
|
|
184
|
+
B: ["C"],
|
|
185
|
+
C: ["A"],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
topologicalSort(dependencies);
|
|
190
|
+
expect(true).toBe(false); // Should not reach here
|
|
191
|
+
} catch (error) {
|
|
192
|
+
expect(error).toBeInstanceOf(BuildError);
|
|
193
|
+
if (error instanceof BuildError) {
|
|
194
|
+
expect(error.message).toContain("Circular dependency detected");
|
|
195
|
+
expect(error.message).toContain("->");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("sorts disconnected components", () => {
|
|
201
|
+
const dependencies = {
|
|
202
|
+
A: [],
|
|
203
|
+
B: ["A"],
|
|
204
|
+
C: [],
|
|
205
|
+
D: ["C"],
|
|
206
|
+
};
|
|
207
|
+
const result = topologicalSort(dependencies);
|
|
208
|
+
|
|
209
|
+
// Check ordering within each component
|
|
210
|
+
const aIndex = result.indexOf("A");
|
|
211
|
+
const bIndex = result.indexOf("B");
|
|
212
|
+
const cIndex = result.indexOf("C");
|
|
213
|
+
const dIndex = result.indexOf("D");
|
|
214
|
+
|
|
215
|
+
expect(aIndex).toBeLessThan(bIndex);
|
|
216
|
+
expect(cIndex).toBeLessThan(dIndex);
|
|
217
|
+
expect(result).toHaveLength(4);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("handles nodes with empty dependency arrays", () => {
|
|
221
|
+
const dependencies = {
|
|
222
|
+
A: [],
|
|
223
|
+
B: [],
|
|
224
|
+
C: ["A", "B"],
|
|
225
|
+
};
|
|
226
|
+
const result = topologicalSort(dependencies);
|
|
227
|
+
|
|
228
|
+
const cIndex = result.indexOf("C");
|
|
229
|
+
expect(result.indexOf("A")).toBeLessThan(cIndex);
|
|
230
|
+
expect(result.indexOf("B")).toBeLessThan(cIndex);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("preserves all nodes in result", () => {
|
|
234
|
+
const dependencies = {
|
|
235
|
+
A: [],
|
|
236
|
+
B: ["A"],
|
|
237
|
+
C: ["A"],
|
|
238
|
+
D: ["B", "C"],
|
|
239
|
+
E: [],
|
|
240
|
+
F: ["E"],
|
|
241
|
+
};
|
|
242
|
+
const result = topologicalSort(dependencies);
|
|
243
|
+
|
|
244
|
+
expect(result).toHaveLength(6);
|
|
245
|
+
expect(new Set(result).size).toBe(6); // No duplicates
|
|
246
|
+
expect(result).toContain("A");
|
|
247
|
+
expect(result).toContain("B");
|
|
248
|
+
expect(result).toContain("C");
|
|
249
|
+
expect(result).toContain("D");
|
|
250
|
+
expect(result).toContain("E");
|
|
251
|
+
expect(result).toContain("F");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("handles dependencies on non-existent nodes", () => {
|
|
255
|
+
const dependencies = {
|
|
256
|
+
A: ["B"], // B doesn't exist as a key
|
|
257
|
+
C: [],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Should handle gracefully - B is referenced but not defined
|
|
261
|
+
const result = topologicalSort(dependencies);
|
|
262
|
+
expect(result).toContain("A");
|
|
263
|
+
expect(result).toContain("C");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("handles realistic CloudFormation-style resource dependencies", () => {
|
|
267
|
+
const dependencies = {
|
|
268
|
+
MyVPC: [],
|
|
269
|
+
MySubnet: ["MyVPC"],
|
|
270
|
+
MySecurityGroup: ["MyVPC"],
|
|
271
|
+
MyInstance: ["MySubnet", "MySecurityGroup"],
|
|
272
|
+
MyEIP: ["MyInstance"],
|
|
273
|
+
};
|
|
274
|
+
const result = topologicalSort(dependencies);
|
|
275
|
+
|
|
276
|
+
// VPC must be first
|
|
277
|
+
expect(result[0]).toBe("MyVPC");
|
|
278
|
+
|
|
279
|
+
// EIP must be last
|
|
280
|
+
expect(result[result.length - 1]).toBe("MyEIP");
|
|
281
|
+
|
|
282
|
+
// Subnet and SecurityGroup must come before Instance
|
|
283
|
+
const subnetIndex = result.indexOf("MySubnet");
|
|
284
|
+
const sgIndex = result.indexOf("MySecurityGroup");
|
|
285
|
+
const instanceIndex = result.indexOf("MyInstance");
|
|
286
|
+
|
|
287
|
+
expect(subnetIndex).toBeLessThan(instanceIndex);
|
|
288
|
+
expect(sgIndex).toBeLessThan(instanceIndex);
|
|
289
|
+
});
|
|
290
|
+
});
|
package/src/sort.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BuildError } from "./errors";
|
|
2
|
+
import { detectCycles } from "./discovery/cycles";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Performs a topological sort on a dependency graph
|
|
6
|
+
* @param dependencies - Record where keys are entity names and values are arrays of their dependencies
|
|
7
|
+
* @returns Array of entity names in topological order (dependencies appear before dependents)
|
|
8
|
+
* @throws {BuildError} If a cycle is detected in the dependency graph
|
|
9
|
+
*/
|
|
10
|
+
export function topologicalSort(
|
|
11
|
+
dependencies: Record<string, string[]>
|
|
12
|
+
): string[] {
|
|
13
|
+
// Check for cycles first
|
|
14
|
+
const cycles = detectCycles(dependencies);
|
|
15
|
+
if (cycles.length > 0) {
|
|
16
|
+
const cycleStr = cycles[0].join(" -> ");
|
|
17
|
+
throw new BuildError(
|
|
18
|
+
cycles[0][0],
|
|
19
|
+
`Circular dependency detected: ${cycleStr}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sorted: string[] = [];
|
|
24
|
+
const visited = new Set<string>();
|
|
25
|
+
const visiting = new Set<string>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* DFS helper for topological sort
|
|
29
|
+
* @param node - Current node being visited
|
|
30
|
+
*/
|
|
31
|
+
function visit(node: string): void {
|
|
32
|
+
if (visited.has(node)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
visiting.add(node);
|
|
37
|
+
|
|
38
|
+
const deps = dependencies[node] || [];
|
|
39
|
+
for (const dep of deps) {
|
|
40
|
+
if (!visited.has(dep)) {
|
|
41
|
+
visit(dep);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
visiting.delete(node);
|
|
46
|
+
visited.add(node);
|
|
47
|
+
sorted.push(node);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Visit all nodes
|
|
51
|
+
for (const node of Object.keys(dependencies)) {
|
|
52
|
+
if (!visited.has(node)) {
|
|
53
|
+
visit(node);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sorted;
|
|
58
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack Output — marks a value for cross-stack export.
|
|
3
|
+
*
|
|
4
|
+
* When a child project declares `stackOutput(ref)`, the serializer emits
|
|
5
|
+
* it into the template's Outputs section. The parent can then reference
|
|
6
|
+
* it via `nestedStack().outputs.name`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
10
|
+
import type { AttrRef } from "./attrref";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Marker symbol for stack output identification.
|
|
14
|
+
*/
|
|
15
|
+
export const STACK_OUTPUT_MARKER = Symbol.for("chant.stackOutput");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A stack output declaration — wraps an AttrRef into a Declarable
|
|
19
|
+
* that serializers emit as a template Output.
|
|
20
|
+
*/
|
|
21
|
+
export interface StackOutput extends Declarable {
|
|
22
|
+
readonly [STACK_OUTPUT_MARKER]: true;
|
|
23
|
+
readonly [DECLARABLE_MARKER]: true;
|
|
24
|
+
readonly lexicon: string;
|
|
25
|
+
readonly entityType: string;
|
|
26
|
+
readonly kind: "output";
|
|
27
|
+
readonly sourceRef: AttrRef;
|
|
28
|
+
readonly description?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Type guard for StackOutput.
|
|
33
|
+
*/
|
|
34
|
+
export function isStackOutput(value: unknown): value is StackOutput {
|
|
35
|
+
return (
|
|
36
|
+
typeof value === "object" &&
|
|
37
|
+
value !== null &&
|
|
38
|
+
STACK_OUTPUT_MARKER in value &&
|
|
39
|
+
(value as Record<symbol, unknown>)[STACK_OUTPUT_MARKER] === true
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a stack output that exports an attribute reference for cross-stack use.
|
|
45
|
+
*
|
|
46
|
+
* @param ref - The AttrRef to export (e.g. `vpc.vpcId`)
|
|
47
|
+
* @param options - Optional description for the output
|
|
48
|
+
* @returns A StackOutput Declarable
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { stackOutput } from "@intentius/chant";
|
|
53
|
+
* import * as _ from "./_";
|
|
54
|
+
*
|
|
55
|
+
* export const vpcId = stackOutput(_.$.vpc.vpcId);
|
|
56
|
+
* export const subnetId = stackOutput(_.$.subnet.subnetId, {
|
|
57
|
+
* description: "Primary subnet ID",
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function stackOutput(
|
|
62
|
+
ref: AttrRef,
|
|
63
|
+
options?: { description?: string },
|
|
64
|
+
): StackOutput {
|
|
65
|
+
// Derive lexicon from the AttrRef's parent entity
|
|
66
|
+
const parent = ref.parent.deref();
|
|
67
|
+
const lexicon = parent && typeof (parent as any).lexicon === "string"
|
|
68
|
+
? (parent as any).lexicon
|
|
69
|
+
: "unknown";
|
|
70
|
+
|
|
71
|
+
const output: StackOutput = {
|
|
72
|
+
[STACK_OUTPUT_MARKER]: true,
|
|
73
|
+
[DECLARABLE_MARKER]: true,
|
|
74
|
+
lexicon,
|
|
75
|
+
entityType: "chant:output",
|
|
76
|
+
kind: "output",
|
|
77
|
+
sourceRef: ref,
|
|
78
|
+
description: options?.description,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return output;
|
|
82
|
+
}
|