@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.
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +14 -9
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
- package/dist/discovery/graph.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-wire.d.ts +16 -0
- package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.d.ts +29 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +18 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/sandbox/policy-run.d.ts +33 -0
- package/dist/discovery/sandbox/policy-run.d.ts.map +1 -0
- package/dist/discovery/sandbox/policy-wire.d.ts +177 -0
- package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -0
- package/dist/intrinsic-interpolation.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lint/policy-import.d.ts +50 -0
- package/dist/lint/policy-import.d.ts.map +1 -0
- package/dist/lint/policy-sandbox.d.ts +89 -0
- package/dist/lint/policy-sandbox.d.ts.map +1 -0
- package/dist/lint/policy.d.ts +12 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.test.ts +77 -1
- package/src/build.ts +15 -2
- package/src/cli/commands/build.ts +33 -5
- package/src/cli/main.test.ts +93 -17
- package/src/cli/main.ts +101 -11
- package/src/discovery/entity-wire-codec.ts +14 -9
- package/src/discovery/graph.test.ts +40 -1
- package/src/discovery/graph.ts +8 -2
- package/src/discovery/sandbox/config-wire.ts +21 -0
- package/src/discovery/sandbox/driver.ts +132 -0
- package/src/discovery/sandbox/fork.ts +39 -1
- package/src/discovery/sandbox/policy-boundary.test.ts +325 -0
- package/src/discovery/sandbox/policy-run.ts +180 -0
- package/src/discovery/sandbox/policy-wire.test.ts +310 -0
- package/src/discovery/sandbox/policy-wire.ts +277 -0
- package/src/intrinsic-interpolation.test.ts +27 -1
- package/src/intrinsic-interpolation.ts +10 -2
- package/src/lexicon-output.test.ts +36 -0
- package/src/lexicon-output.ts +12 -2
- package/src/lint/policy-import.ts +70 -0
- package/src/lint/policy-sandbox.ts +123 -0
- package/src/lint/policy.ts +20 -2
- package/src/stack-output.test.ts +118 -0
- 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
|
+
});
|
package/src/stack-output.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|