@intentius/chant 0.27.0 → 0.29.0

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.
Files changed (106) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/components.d.ts.map +1 -1
  4. package/dist/cli/handlers/graph.d.ts.map +1 -1
  5. package/dist/cli/handlers/lifecycle.d.ts +5 -3
  6. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  7. package/dist/config.d.ts +46 -4
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/discovery/fold-import.d.ts +153 -17
  10. package/dist/discovery/fold-import.d.ts.map +1 -1
  11. package/dist/discovery/sandbox/config-run.d.ts.map +1 -1
  12. package/dist/discovery/sandbox/config-wire.d.ts +3 -2
  13. package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
  14. package/dist/discovery/sandbox/fork.d.ts +25 -0
  15. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  16. package/dist/discovery/sandbox/policy-run.d.ts.map +1 -1
  17. package/dist/discovery/sandbox/run.d.ts.map +1 -1
  18. package/dist/env.d.ts +5 -2
  19. package/dist/env.d.ts.map +1 -1
  20. package/dist/fold/fold.d.ts +12 -0
  21. package/dist/fold/fold.d.ts.map +1 -1
  22. package/dist/graph-ir.d.ts +29 -4
  23. package/dist/graph-ir.d.ts.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/kubectl-context.d.ts +100 -0
  27. package/dist/kubectl-context.d.ts.map +1 -0
  28. package/dist/lexicon.d.ts +31 -6
  29. package/dist/lexicon.d.ts.map +1 -1
  30. package/dist/lifecycle/change-set.d.ts +26 -5
  31. package/dist/lifecycle/change-set.d.ts.map +1 -1
  32. package/dist/lifecycle/live-diff.d.ts +25 -1
  33. package/dist/lifecycle/live-diff.d.ts.map +1 -1
  34. package/dist/lifecycle/observe.d.ts +4 -2
  35. package/dist/lifecycle/observe.d.ts.map +1 -1
  36. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  37. package/dist/lifecycle/status.d.ts +26 -1
  38. package/dist/lifecycle/status.d.ts.map +1 -1
  39. package/dist/lifecycle/types.d.ts +8 -0
  40. package/dist/lifecycle/types.d.ts.map +1 -1
  41. package/dist/lint/config.d.ts +80 -0
  42. package/dist/lint/config.d.ts.map +1 -1
  43. package/dist/lint/policy.d.ts +8 -2
  44. package/dist/lint/policy.d.ts.map +1 -1
  45. package/dist/lint/post-synth.d.ts +18 -1
  46. package/dist/lint/post-synth.d.ts.map +1 -1
  47. package/dist/live-endpoint.d.ts +92 -0
  48. package/dist/live-endpoint.d.ts.map +1 -0
  49. package/dist/observation.d.ts +123 -0
  50. package/dist/observation.d.ts.map +1 -0
  51. package/dist/stack-output.d.ts +9 -4
  52. package/dist/stack-output.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/cli/commands/build.test.ts +190 -0
  55. package/src/cli/commands/build.ts +34 -2
  56. package/src/cli/commands/lint.ts +17 -25
  57. package/src/cli/handlers/components.test.ts +63 -4
  58. package/src/cli/handlers/components.ts +78 -35
  59. package/src/cli/handlers/graph.test.ts +69 -6
  60. package/src/cli/handlers/graph.ts +61 -27
  61. package/src/cli/handlers/lifecycle.test.ts +285 -6
  62. package/src/cli/handlers/lifecycle.ts +297 -185
  63. package/src/config.test.ts +75 -0
  64. package/src/config.ts +61 -3
  65. package/src/discovery/fold-composite.test.ts +594 -0
  66. package/src/discovery/fold-import.ts +987 -43
  67. package/src/discovery/sandbox/config-boundary.test.ts +55 -1
  68. package/src/discovery/sandbox/config-run.ts +3 -0
  69. package/src/discovery/sandbox/config-wire.ts +3 -2
  70. package/src/discovery/sandbox/fork.ts +75 -1
  71. package/src/discovery/sandbox/policy-boundary.test.ts +56 -1
  72. package/src/discovery/sandbox/policy-run.ts +15 -1
  73. package/src/discovery/sandbox/run.test.ts +85 -1
  74. package/src/discovery/sandbox/run.ts +3 -0
  75. package/src/env.test.ts +12 -0
  76. package/src/env.ts +12 -4
  77. package/src/fold/fold.ts +12 -2
  78. package/src/graph-ir-live.test.ts +28 -1
  79. package/src/graph-ir.ts +68 -12
  80. package/src/index.ts +1 -0
  81. package/src/kubectl-context.test.ts +94 -0
  82. package/src/kubectl-context.ts +207 -0
  83. package/src/lexicon.ts +41 -6
  84. package/src/lifecycle/change-set.test.ts +93 -1
  85. package/src/lifecycle/change-set.ts +65 -13
  86. package/src/lifecycle/live-diff.test.ts +39 -0
  87. package/src/lifecycle/live-diff.ts +51 -5
  88. package/src/lifecycle/observe.test.ts +74 -3
  89. package/src/lifecycle/observe.ts +82 -22
  90. package/src/lifecycle/snapshot.test.ts +39 -1
  91. package/src/lifecycle/snapshot.ts +34 -9
  92. package/src/lifecycle/status.test.ts +89 -8
  93. package/src/lifecycle/status.ts +53 -3
  94. package/src/lifecycle/types.ts +8 -0
  95. package/src/lint/config.test.ts +93 -1
  96. package/src/lint/config.ts +108 -0
  97. package/src/lint/policy.test.ts +90 -0
  98. package/src/lint/policy.ts +17 -5
  99. package/src/lint/post-synth.test.ts +4 -0
  100. package/src/lint/post-synth.ts +30 -1
  101. package/src/live-endpoint.test.ts +115 -0
  102. package/src/live-endpoint.ts +148 -0
  103. package/src/observation.test.ts +96 -0
  104. package/src/observation.ts +213 -0
  105. package/src/stack-output.test.ts +76 -3
  106. package/src/stack-output.ts +59 -26
@@ -41,9 +41,9 @@ describe("stackOutput", () => {
41
41
  expect(out.description).toBe("Primary VPC id");
42
42
  });
43
43
 
44
- test("throws for a value that is neither AttrRef-like nor an Intrinsic", () => {
45
- expect(() => stackOutput("not-a-ref" as unknown as AttrRef)).toThrow(
46
- "stackOutput(ref): ref must be an attribute reference or an intrinsic wrapping one",
44
+ test("throws for a value that is neither AttrRef-like, Intrinsic, nor a string literal", () => {
45
+ expect(() => stackOutput(42 as unknown as AttrRef)).toThrow(
46
+ "stackOutput(ref): ref must be an attribute reference, an intrinsic wrapping one, or a literal string",
47
47
  );
48
48
  });
49
49
 
@@ -97,6 +97,61 @@ describe("stackOutput", () => {
97
97
  expect(out.lexicon).toBe("aws");
98
98
  vi.resetModules();
99
99
  });
100
+
101
+ // chant #1152 — a CloudFormation Parameter carries no attributes at all, so
102
+ // `Ref(param)` (an intrinsic wrapping the Parameter Declarable directly, not
103
+ // one of its attributes) could never contain a nested AttrRef for the old
104
+ // AttrRef-only anchor search to find. Before the fix that meant `firstAttrRef`
105
+ // always came back empty for this shape and the output's `lexicon` silently
106
+ // became `"unknown"` — not a thrown error (a Parameter-wrapping `Ref` already
107
+ // carries the `Intrinsic` marker, so the input-validation guard always
108
+ // accepted it), but `"unknown"` isn't any real lexicon's partition, so the
109
+ // output was silently dropped from every serializer's `Outputs` section.
110
+ const param: Declarable = {
111
+ lexicon: "aws",
112
+ entityType: "AWS::CloudFormation::Parameter",
113
+ [DECLARABLE_MARKER]: true,
114
+ };
115
+
116
+ test("derives lexicon from a Declarable referenced directly by a wrapping intrinsic (Ref(param))", () => {
117
+ const ref = {
118
+ [INTRINSIC_MARKER]: true as const,
119
+ target: param,
120
+ toJSON: () => ({ Ref: "environment" }),
121
+ };
122
+
123
+ const out = stackOutput(ref, { exportName: "EnvironmentName" });
124
+ expect(out.lexicon).toBe("aws");
125
+ expect(out.sourceRef).toBe(ref);
126
+ expect(out.exportName).toBe("EnvironmentName");
127
+ });
128
+
129
+ test("derives lexicon from a Declarable referenced directly, nested inside a further intrinsic (Sub interpolating Ref(param))", () => {
130
+ const ref = {
131
+ [INTRINSIC_MARKER]: true as const,
132
+ target: param,
133
+ toJSON: () => ({ Ref: "environment" }),
134
+ };
135
+ const sub = {
136
+ [INTRINSIC_MARKER]: true as const,
137
+ values: [ref],
138
+ toJSON: () => ({ "Fn::Sub": "env-${environment}" }),
139
+ };
140
+
141
+ const out = stackOutput(sub);
142
+ expect(out.lexicon).toBe("aws");
143
+ });
144
+
145
+ test("an explicit options.lexicon still wins over a Declarable-derived anchor", () => {
146
+ const ref = {
147
+ [INTRINSIC_MARKER]: true as const,
148
+ target: param,
149
+ toJSON: () => ({ Ref: "environment" }),
150
+ };
151
+
152
+ const out = stackOutput(ref, { lexicon: "gcp" });
153
+ expect(out.lexicon).toBe("gcp");
154
+ });
100
155
  });
101
156
 
102
157
  describe("isStackOutput", () => {
@@ -116,3 +171,21 @@ describe("isStackOutput", () => {
116
171
  expect(isStackOutput(42)).toBe(false);
117
172
  });
118
173
  });
174
+
175
+ describe("stackOutput export names and literals", () => {
176
+ test("carries exportName through to the declaration", () => {
177
+ const ref = new AttrRef(vpc, "VpcId");
178
+ const out = stackOutput(ref, { exportName: "my-stack-VpcId" });
179
+ expect(out.exportName).toBe("my-stack-VpcId");
180
+ });
181
+
182
+ test("accepts a literal string with an explicit lexicon", () => {
183
+ const out = stackOutput("22", { lexicon: "aws", exportName: "my-stack-OpenSSHPort" });
184
+ expect(out.sourceRef).toBe("22");
185
+ expect(out.lexicon).toBe("aws");
186
+ });
187
+
188
+ test("rejects a literal without a lexicon", () => {
189
+ expect(() => stackOutput("22")).toThrow(/options\.lexicon/);
190
+ });
191
+ });
@@ -6,7 +6,7 @@
6
6
  * it via `nestedStack().outputs.name`.
7
7
  */
8
8
 
9
- import { DECLARABLE_MARKER, type Declarable } from "./declarable";
9
+ import { DECLARABLE_MARKER, isDeclarable, type Declarable } from "./declarable";
10
10
  import { AttrRef } from "./attrref";
11
11
  import { isIntrinsic, type Intrinsic } from "./intrinsic";
12
12
  import { isAttrRefLike } from "./utils";
@@ -26,31 +26,56 @@ export interface StackOutput extends Declarable {
26
26
  readonly lexicon: string;
27
27
  readonly entityType: string;
28
28
  readonly kind: "output";
29
- /** The exported value: a bare attribute reference, or an intrinsic wrapping
30
- * one (e.g. `Join(",", zone.NameServers)`). */
31
- readonly sourceRef: AttrRef | Intrinsic;
29
+ /** The exported value: a bare attribute reference, an intrinsic wrapping
30
+ * one (e.g. `Join(",", zone.NameServers)`), or a literal string. */
31
+ readonly sourceRef: AttrRef | Intrinsic | string;
32
32
  readonly description?: string;
33
+ /** When set, the serializer emits a cross-stack export under this name
34
+ * (CloudFormation `Output.Export.Name`) in addition to the plain output. */
35
+ readonly exportName?: string;
33
36
  }
34
37
 
35
- /** Find the first AttrRef anywhere inside a value (walking intrinsics/objects/
36
- * arrays), so a wrapping intrinsic can still borrow its parent's lexicon.
37
- * Duck-type, not `instanceof` (chant #1137): a lexicon built against a
38
- * separate copy of `@intentius/chant` produces AttrRefs that fail
39
- * `instanceof AttrRef` here but carry the same shape — missing one would
40
- * make the walk recurse into the AttrRef's own (unhelpful) fields instead
41
- * of stopping on it, silently failing to find the anchor. */
42
- function firstAttrRef(value: unknown, seen = new Set<unknown>()): AttrRef | undefined {
38
+ /** What a wrapping intrinsic's lexicon gets derived from: either a nested
39
+ * AttrRef (borrow its parent's `.lexicon`) or a Declarable referenced
40
+ * directly (use its own `.lexicon`). Two shapes, not one, because
41
+ * `Ref(someEntity)` (chant #1152) wraps the entity itself — a
42
+ * CloudFormation Parameter carries no attributes at all, so `Ref(param)`
43
+ * can never contain a nested AttrRef for a single-shape search to find. */
44
+ type Anchor = { readonly kind: "attrRef"; readonly ref: AttrRef } | { readonly kind: "declarable"; readonly entity: Declarable };
45
+
46
+ /** Find the first AttrRef or directly-referenced Declarable anywhere inside a
47
+ * value (walking intrinsics/objects/arrays), so a wrapping intrinsic can
48
+ * still borrow its source's lexicon. Duck-type, not `instanceof` (chant
49
+ * #1137): a lexicon built against a separate copy of `@intentius/chant`
50
+ * produces AttrRefs/Declarables that fail `instanceof` here but carry the
51
+ * same shape (`isDeclarable`'s `Symbol.for` marker is already dual-copy-safe
52
+ * on its own) — missing one would make the walk recurse into its own
53
+ * (unhelpful) fields instead of stopping on it, silently failing to find the
54
+ * anchor. */
55
+ function findAnchor(value: unknown, seen = new Set<unknown>()): Anchor | undefined {
43
56
  if (value === null || typeof value !== "object" || seen.has(value)) return undefined;
44
- if (isAttrRefLike(value)) return value;
57
+ if (isAttrRefLike(value)) return { kind: "attrRef", ref: value };
58
+ if (isDeclarable(value)) return { kind: "declarable", entity: value };
45
59
  seen.add(value);
46
60
  const children = Array.isArray(value) ? value : Object.values(value as Record<string, unknown>);
47
61
  for (const child of children) {
48
- const found = firstAttrRef(child, seen);
62
+ const found = findAnchor(child, seen);
49
63
  if (found) return found;
50
64
  }
51
65
  return undefined;
52
66
  }
53
67
 
68
+ /** Derive the lexicon an anchor belongs to: an AttrRef borrows its (deref'd)
69
+ * parent's `.lexicon`; a directly-referenced Declarable reports its own. */
70
+ function anchorLexicon(anchor: Anchor | undefined): string {
71
+ if (!anchor) return "unknown";
72
+ if (anchor.kind === "declarable") return anchor.entity.lexicon;
73
+ const parent = anchor.ref.parent.deref();
74
+ return parent && typeof (parent as Record<string, unknown>).lexicon === "string"
75
+ ? ((parent as Record<string, unknown>).lexicon as string)
76
+ : "unknown";
77
+ }
78
+
54
79
  /**
55
80
  * Type guard for StackOutput.
56
81
  */
@@ -82,8 +107,8 @@ export function isStackOutput(value: unknown): value is StackOutput {
82
107
  * ```
83
108
  */
84
109
  export function stackOutput(
85
- ref: AttrRef | Intrinsic,
86
- options?: { description?: string },
110
+ ref: AttrRef | Intrinsic | string,
111
+ options?: { description?: string; exportName?: string; lexicon?: string },
87
112
  ): StackOutput {
88
113
  // Duck-type, not `instanceof` (chant #1137): AttrRef also implements
89
114
  // Intrinsic (a global-symbol marker), so `isIntrinsic(ref)` alone already
@@ -92,19 +117,26 @@ export function stackOutput(
92
117
  // so the guard's own logic states the invariant explicitly ("ref must be
93
118
  // AttrRef-like or Intrinsic") rather than relying on that coincidence,
94
119
  // matching the anchor selection right below it, which does misbehave.
95
- if (!isAttrRefLike(ref) && !isIntrinsic(ref)) {
96
- throw new Error("stackOutput(ref): ref must be an attribute reference or an intrinsic wrapping one");
120
+ if (typeof ref !== "string" && !isAttrRefLike(ref) && !isIntrinsic(ref)) {
121
+ throw new Error(
122
+ "stackOutput(ref): ref must be an attribute reference, an intrinsic wrapping one, or a literal string",
123
+ );
124
+ }
125
+ if (typeof ref === "string" && !options?.lexicon) {
126
+ throw new Error(
127
+ "stackOutput(literal): a literal output has no entity to derive its lexicon from — pass options.lexicon",
128
+ );
97
129
  }
98
130
  // Derive lexicon from the referenced entity — for a bare AttrRef, its parent;
99
- // for an intrinsic (Join etc.), the first AttrRef nested inside it. A
100
- // foreign-copy AttrRef failing raw `instanceof` here would fall to
101
- // `firstAttrRef`, which (before its own #1137 fix) would also miss it —
131
+ // for an intrinsic (Join etc.), the first AttrRef or directly-referenced
132
+ // Declarable nested inside it (chant #1152: `Ref(param)`/`Ref(resource)`
133
+ // wrap the entity itself, not one of its attributes). A foreign-copy
134
+ // AttrRef/Declarable failing raw `instanceof` here would fall to
135
+ // `findAnchor`, which (before its own #1137 fix) would also miss it —
102
136
  // silently anchoring on nothing and recording `lexicon: "unknown"`.
103
- const anchor = isAttrRefLike(ref) ? ref : firstAttrRef(ref);
104
- const parent = anchor?.parent.deref();
105
- const lexicon = parent && typeof (parent as Record<string, unknown>).lexicon === "string"
106
- ? (parent as Record<string, unknown>).lexicon as string
107
- : "unknown";
137
+ const anchor: Anchor | undefined =
138
+ typeof ref === "string" ? undefined : isAttrRefLike(ref) ? { kind: "attrRef", ref } : findAnchor(ref);
139
+ const lexicon = options?.lexicon ?? anchorLexicon(anchor);
108
140
 
109
141
  const output: StackOutput = {
110
142
  [STACK_OUTPUT_MARKER]: true,
@@ -114,6 +146,7 @@ export function stackOutput(
114
146
  kind: "output",
115
147
  sourceRef: ref,
116
148
  description: options?.description,
149
+ exportName: options?.exportName,
117
150
  };
118
151
 
119
152
  return output;