@intentius/chant 0.25.0 → 0.26.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 (50) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/cli/commands/build.d.ts.map +1 -1
  3. package/dist/cli/main.d.ts.map +1 -1
  4. package/dist/discovery/entity-wire-codec.d.ts +14 -9
  5. package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
  6. package/dist/discovery/graph.d.ts.map +1 -1
  7. package/dist/discovery/sandbox/config-wire.d.ts +16 -0
  8. package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
  9. package/dist/discovery/sandbox/driver.d.ts +29 -0
  10. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  11. package/dist/discovery/sandbox/fork.d.ts +18 -0
  12. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  13. package/dist/discovery/sandbox/policy-run.d.ts +33 -0
  14. package/dist/discovery/sandbox/policy-run.d.ts.map +1 -0
  15. package/dist/discovery/sandbox/policy-wire.d.ts +177 -0
  16. package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -0
  17. package/dist/intrinsic-interpolation.d.ts.map +1 -1
  18. package/dist/lexicon-output.d.ts.map +1 -1
  19. package/dist/lint/policy-import.d.ts +50 -0
  20. package/dist/lint/policy-import.d.ts.map +1 -0
  21. package/dist/lint/policy-sandbox.d.ts +89 -0
  22. package/dist/lint/policy-sandbox.d.ts.map +1 -0
  23. package/dist/lint/policy.d.ts +12 -1
  24. package/dist/lint/policy.d.ts.map +1 -1
  25. package/dist/stack-output.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/build.test.ts +77 -1
  28. package/src/build.ts +15 -2
  29. package/src/cli/commands/build.ts +33 -5
  30. package/src/cli/main.test.ts +93 -17
  31. package/src/cli/main.ts +101 -11
  32. package/src/discovery/entity-wire-codec.ts +14 -9
  33. package/src/discovery/graph.test.ts +40 -1
  34. package/src/discovery/graph.ts +8 -2
  35. package/src/discovery/sandbox/config-wire.ts +21 -0
  36. package/src/discovery/sandbox/driver.ts +132 -0
  37. package/src/discovery/sandbox/fork.ts +39 -1
  38. package/src/discovery/sandbox/policy-boundary.test.ts +325 -0
  39. package/src/discovery/sandbox/policy-run.ts +180 -0
  40. package/src/discovery/sandbox/policy-wire.test.ts +310 -0
  41. package/src/discovery/sandbox/policy-wire.ts +277 -0
  42. package/src/intrinsic-interpolation.test.ts +27 -1
  43. package/src/intrinsic-interpolation.ts +10 -2
  44. package/src/lexicon-output.test.ts +36 -0
  45. package/src/lexicon-output.ts +12 -2
  46. package/src/lint/policy-import.ts +70 -0
  47. package/src/lint/policy-sandbox.ts +123 -0
  48. package/src/lint/policy.ts +20 -2
  49. package/src/stack-output.test.ts +118 -0
  50. package/src/stack-output.ts +21 -5
@@ -0,0 +1,118 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { stackOutput, isStackOutput, STACK_OUTPUT_MARKER } from "./stack-output";
3
+ import { AttrRef } from "./attrref";
4
+ import { INTRINSIC_MARKER } from "./intrinsic";
5
+ import { DECLARABLE_MARKER, type Declarable } from "./declarable";
6
+
7
+ const vpc: Declarable = {
8
+ lexicon: "aws",
9
+ entityType: "AWS::EC2::VPC",
10
+ [DECLARABLE_MARKER]: true,
11
+ };
12
+
13
+ describe("stackOutput", () => {
14
+ test("wraps a bare AttrRef and derives lexicon from its parent", () => {
15
+ const ref = new AttrRef(vpc, "VpcId");
16
+ const out = stackOutput(ref);
17
+
18
+ expect(out[STACK_OUTPUT_MARKER]).toBe(true);
19
+ expect(out[DECLARABLE_MARKER]).toBe(true);
20
+ expect(out.lexicon).toBe("aws");
21
+ expect(out.kind).toBe("output");
22
+ expect(out.sourceRef).toBe(ref);
23
+ });
24
+
25
+ test("wraps an intrinsic nesting an AttrRef and borrows the AttrRef's lexicon", () => {
26
+ const ref = new AttrRef(vpc, "VpcId");
27
+ const join = {
28
+ [INTRINSIC_MARKER]: true as const,
29
+ values: [ref, "-suffix"],
30
+ toJSON: () => ({ "Fn::Join": ["", ["VpcId", "-suffix"]] }),
31
+ };
32
+
33
+ const out = stackOutput(join);
34
+ expect(out.lexicon).toBe("aws");
35
+ expect(out.sourceRef).toBe(join);
36
+ });
37
+
38
+ test("records an optional description", () => {
39
+ const ref = new AttrRef(vpc, "VpcId");
40
+ const out = stackOutput(ref, { description: "Primary VPC id" });
41
+ expect(out.description).toBe("Primary VPC id");
42
+ });
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",
47
+ );
48
+ });
49
+
50
+ test("falls back to lexicon 'unknown' when the anchor's parent has no lexicon field", () => {
51
+ const ref = new AttrRef({}, "attr");
52
+ const out = stackOutput(ref);
53
+ expect(out.lexicon).toBe("unknown");
54
+ });
55
+
56
+ // chant #1137 — `stackOutput()` used to check `ref instanceof AttrRef` (in
57
+ // both its input validation and its lexicon-deriving anchor selection),
58
+ // which returns false for an AttrRef built by a SEPARATELY-LOADED copy of
59
+ // `./attrref` (the same dual-npm-copy hazard #1122 fixed for
60
+ // `LexiconOutput`). `firstAttrRef` had the same raw `instanceof` check, so
61
+ // even the "wrapped in an intrinsic" fallback path missed a foreign
62
+ // AttrRef too. Before the fix, a foreign-copy AttrRef silently anchored on
63
+ // nothing and the output's `lexicon` field became `"unknown"` instead of
64
+ // the real producing lexicon — no error, just a wrong value.
65
+ // `vi.resetModules()` + a fresh dynamic import reproduces the split module
66
+ // graph exactly.
67
+ test("derives lexicon from a bare AttrRef built by a second, separately-loaded copy", async () => {
68
+ vi.resetModules();
69
+ const secondCopy = await import("./attrref");
70
+ expect(secondCopy.AttrRef).not.toBe(AttrRef);
71
+
72
+ const foreignRef = new secondCopy.AttrRef(vpc, "VpcId");
73
+
74
+ // The historic bug: instanceof fails across separately-loaded copies of
75
+ // chant-core, even though the two classes are structurally identical.
76
+ expect(foreignRef instanceof AttrRef).toBe(false);
77
+
78
+ const out = stackOutput(foreignRef);
79
+ expect(out.lexicon).toBe("aws");
80
+ expect(out.sourceRef).toBe(foreignRef);
81
+ vi.resetModules();
82
+ });
83
+
84
+ test("derives lexicon from a foreign-copy AttrRef nested inside an intrinsic", async () => {
85
+ vi.resetModules();
86
+ const secondCopy = await import("./attrref");
87
+ const foreignRef = new secondCopy.AttrRef(vpc, "VpcId");
88
+ expect(foreignRef instanceof AttrRef).toBe(false);
89
+
90
+ const join = {
91
+ [INTRINSIC_MARKER]: true as const,
92
+ values: [foreignRef, "-suffix"],
93
+ toJSON: () => ({ "Fn::Join": ["", ["VpcId", "-suffix"]] }),
94
+ };
95
+
96
+ const out = stackOutput(join);
97
+ expect(out.lexicon).toBe("aws");
98
+ vi.resetModules();
99
+ });
100
+ });
101
+
102
+ describe("isStackOutput", () => {
103
+ test("returns true for a StackOutput", () => {
104
+ const ref = new AttrRef(vpc, "VpcId");
105
+ expect(isStackOutput(stackOutput(ref))).toBe(true);
106
+ });
107
+
108
+ test("returns false for a plain AttrRef", () => {
109
+ const ref = new AttrRef(vpc, "VpcId");
110
+ expect(isStackOutput(ref)).toBe(false);
111
+ });
112
+
113
+ test("returns false for null/primitives", () => {
114
+ expect(isStackOutput(null)).toBe(false);
115
+ expect(isStackOutput("x")).toBe(false);
116
+ expect(isStackOutput(42)).toBe(false);
117
+ });
118
+ });
@@ -9,6 +9,7 @@
9
9
  import { DECLARABLE_MARKER, type Declarable } from "./declarable";
10
10
  import { AttrRef } from "./attrref";
11
11
  import { isIntrinsic, type Intrinsic } from "./intrinsic";
12
+ import { isAttrRefLike } from "./utils";
12
13
 
13
14
  /**
14
15
  * Marker symbol for stack output identification.
@@ -32,10 +33,15 @@ export interface StackOutput extends Declarable {
32
33
  }
33
34
 
34
35
  /** Find the first AttrRef anywhere inside a value (walking intrinsics/objects/
35
- * arrays), so a wrapping intrinsic can still borrow its parent's lexicon. */
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. */
36
42
  function firstAttrRef(value: unknown, seen = new Set<unknown>()): AttrRef | undefined {
37
43
  if (value === null || typeof value !== "object" || seen.has(value)) return undefined;
38
- if (value instanceof AttrRef) return value;
44
+ if (isAttrRefLike(value)) return value;
39
45
  seen.add(value);
40
46
  const children = Array.isArray(value) ? value : Object.values(value as Record<string, unknown>);
41
47
  for (const child of children) {
@@ -79,12 +85,22 @@ export function stackOutput(
79
85
  ref: AttrRef | Intrinsic,
80
86
  options?: { description?: string },
81
87
  ): StackOutput {
82
- if (!(ref instanceof AttrRef) && !isIntrinsic(ref)) {
88
+ // Duck-type, not `instanceof` (chant #1137): AttrRef also implements
89
+ // Intrinsic (a global-symbol marker), so `isIntrinsic(ref)` alone already
90
+ // happens to accept a foreign-copy AttrRef here — this check would not
91
+ // misfire even with the raw `instanceof` left in. It is converted anyway
92
+ // so the guard's own logic states the invariant explicitly ("ref must be
93
+ // AttrRef-like or Intrinsic") rather than relying on that coincidence,
94
+ // matching the anchor selection right below it, which does misbehave.
95
+ if (!isAttrRefLike(ref) && !isIntrinsic(ref)) {
83
96
  throw new Error("stackOutput(ref): ref must be an attribute reference or an intrinsic wrapping one");
84
97
  }
85
98
  // Derive lexicon from the referenced entity — for a bare AttrRef, its parent;
86
- // for an intrinsic (Join etc.), the first AttrRef nested inside it.
87
- const anchor = ref instanceof AttrRef ? ref : firstAttrRef(ref);
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 —
102
+ // silently anchoring on nothing and recording `lexicon: "unknown"`.
103
+ const anchor = isAttrRefLike(ref) ? ref : firstAttrRef(ref);
88
104
  const parent = anchor?.parent.deref();
89
105
  const lexicon = parent && typeof (parent as Record<string, unknown>).lexicon === "string"
90
106
  ? (parent as Record<string, unknown>).lexicon as string