@intentius/chant 0.19.1 → 0.21.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-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +71 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +239 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +299 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +134 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +4 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +131 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +179 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +137 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +67 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/codegen/docs-types.ts +2 -0
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +487 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1598 -0
- package/src/discovery/fold-import.ts +1998 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +242 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +812 -0
- package/src/fold/fold.ts +805 -0
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +210 -0
- package/src/fold/subset.test.ts +352 -0
- package/src/fold/subset.ts +383 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +57 -0
- package/src/lexicon-schema.ts +8 -1
- package/src/lexicon.ts +132 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
- package/src/runtime.ts +11 -2
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { fold, foldModule, collectConsts, FoldError, type FoldedValue } from "./fold";
|
|
4
|
+
import { createResource } from "../runtime";
|
|
5
|
+
import { resolveAttrRefs } from "../discovery/resolve";
|
|
6
|
+
import type { Declarable } from "../declarable";
|
|
7
|
+
import type { AttrRef } from "../attrref";
|
|
8
|
+
import type { IntrinsicDef } from "../lexicon";
|
|
9
|
+
|
|
10
|
+
/** Parse a snippet and return its top-level `const` map, for direct `fold()` tests. */
|
|
11
|
+
function parseConsts(source: string): Map<string, ts.Expression> {
|
|
12
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
13
|
+
return collectConsts(sourceFile);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fold the initializer of the named top-level `const` in `source`. */
|
|
17
|
+
function foldConst(source: string, name: string): FoldedValue {
|
|
18
|
+
const consts = parseConsts(source);
|
|
19
|
+
const expr = consts.get(name);
|
|
20
|
+
if (!expr) throw new Error(`fixture error: no such const "${name}" in source`);
|
|
21
|
+
return fold(expr, consts);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("fold — literals", () => {
|
|
25
|
+
test("string literal", () => {
|
|
26
|
+
expect(foldConst(`const x = "hello";`, "x")).toBe("hello");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("numeric literal", () => {
|
|
30
|
+
expect(foldConst(`const x = 42;`, "x")).toBe(42);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("boolean literals", () => {
|
|
34
|
+
expect(foldConst(`const x = true;`, "x")).toBe(true);
|
|
35
|
+
expect(foldConst(`const x = false;`, "x")).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("null", () => {
|
|
39
|
+
expect(foldConst(`const x = null;`, "x")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("undefined", () => {
|
|
43
|
+
expect(foldConst(`const x = undefined;`, "x")).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("no-substitution template literal", () => {
|
|
47
|
+
expect(foldConst("const x = `plain`;", "x")).toBe("plain");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("fold — template interpolation", () => {
|
|
52
|
+
test("interpolates a const identifier", () => {
|
|
53
|
+
const src = `
|
|
54
|
+
const name = "World";
|
|
55
|
+
const x = \`Hello \${name}!\`;
|
|
56
|
+
`;
|
|
57
|
+
expect(foldConst(src, "x")).toBe("Hello World!");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("interpolates multiple spans and coerces non-strings", () => {
|
|
61
|
+
const src = `
|
|
62
|
+
const count = 3;
|
|
63
|
+
const x = \`count=\${count}, ok=\${true}\`;
|
|
64
|
+
`;
|
|
65
|
+
expect(foldConst(src, "x")).toBe("count=3, ok=true");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("fold — objects and spread", () => {
|
|
70
|
+
test("object literal with property and shorthand assignment", () => {
|
|
71
|
+
const src = `
|
|
72
|
+
const b = 2;
|
|
73
|
+
const x = { a: 1, b };
|
|
74
|
+
`;
|
|
75
|
+
expect(foldConst(src, "x")).toEqual({ a: 1, b: 2 });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("spreads a folded object", () => {
|
|
79
|
+
const src = `
|
|
80
|
+
const base = { a: 1, b: 2 };
|
|
81
|
+
const x = { ...base, b: 3, c: 4 };
|
|
82
|
+
`;
|
|
83
|
+
expect(foldConst(src, "x")).toEqual({ a: 1, b: 3, c: 4 });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("nested objects", () => {
|
|
87
|
+
const src = `const x = { a: { b: { c: 1, d: [1, 2] } } };`;
|
|
88
|
+
expect(foldConst(src, "x")).toEqual({ a: { b: { c: 1, d: [1, 2] } } });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("fold — arrays and spread", () => {
|
|
93
|
+
test("array literal", () => {
|
|
94
|
+
expect(foldConst(`const x = [1, "two", true];`, "x")).toEqual([1, "two", true]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("spreads a folded array in place", () => {
|
|
98
|
+
const src = `
|
|
99
|
+
const rest = [2, 3];
|
|
100
|
+
const x = [1, ...rest, 4];
|
|
101
|
+
`;
|
|
102
|
+
expect(foldConst(src, "x")).toEqual([1, 2, 3, 4]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("fold — const identifier resolution", () => {
|
|
107
|
+
test("resolves a chain of const identifiers", () => {
|
|
108
|
+
const src = `
|
|
109
|
+
const a = 5;
|
|
110
|
+
const b = a;
|
|
111
|
+
const c = b;
|
|
112
|
+
`;
|
|
113
|
+
expect(foldConst(src, "c")).toBe(5);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("fold — cross-resource attribute refs", () => {
|
|
118
|
+
test("property access on a const bound to `new` becomes a symbolic ref", () => {
|
|
119
|
+
const src = `
|
|
120
|
+
const bucket = new S3Bucket({ name: "my-bucket" });
|
|
121
|
+
const ref = bucket.name;
|
|
122
|
+
`;
|
|
123
|
+
expect(foldConst(src, "ref")).toEqual({ __attrRef: { entity: "bucket", attribute: "name" } });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("property access on a plain object indexes the folded value", () => {
|
|
127
|
+
const src = `
|
|
128
|
+
const obj = { name: "plain" };
|
|
129
|
+
const x = obj.name;
|
|
130
|
+
`;
|
|
131
|
+
expect(foldConst(src, "x")).toBe("plain");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("differential: fold(bucket.arn) matches the AttrRef.toJSON() the run path yields", () => {
|
|
135
|
+
// Run path: build a real resource instance the way generated lexicon
|
|
136
|
+
// code does (runtime.createResource), then resolve it exactly like
|
|
137
|
+
// discovery does for a real project (discovery/resolve.ts).
|
|
138
|
+
const Bucket = createResource("S3Bucket", "aws", { arn: "arn" });
|
|
139
|
+
const bucket = new Bucket({ name: "my-bucket" }) as unknown as Declarable & { arn: AttrRef };
|
|
140
|
+
const entities = new Map<string, Declarable>([["bucket", bucket]]);
|
|
141
|
+
resolveAttrRefs(entities);
|
|
142
|
+
const runResult = bucket.arn.toJSON();
|
|
143
|
+
|
|
144
|
+
// Fold path: the same source, folded with no module execution.
|
|
145
|
+
const src = `
|
|
146
|
+
const bucket = new S3Bucket({ name: "my-bucket" });
|
|
147
|
+
const ref = bucket.arn;
|
|
148
|
+
`;
|
|
149
|
+
const foldResult = foldConst(src, "ref");
|
|
150
|
+
|
|
151
|
+
expect(foldResult).toEqual(runResult);
|
|
152
|
+
expect(foldResult).toEqual({ __attrRef: { entity: "bucket", attribute: "arn" } });
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("fold — element access", () => {
|
|
157
|
+
test("string literal key on a plain object indexes the folded value", () => {
|
|
158
|
+
const src = `
|
|
159
|
+
const obj = { name: "plain" };
|
|
160
|
+
const x = obj["name"];
|
|
161
|
+
`;
|
|
162
|
+
expect(foldConst(src, "x")).toBe("plain");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("numeric literal key indexes a folded array", () => {
|
|
166
|
+
const src = `
|
|
167
|
+
const arr = ["a", "b", "c"];
|
|
168
|
+
const x = arr[1];
|
|
169
|
+
`;
|
|
170
|
+
expect(foldConst(src, "x")).toBe("b");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("element access on a const bound to `new` becomes a symbolic ref, same as dot access", () => {
|
|
174
|
+
const src = `
|
|
175
|
+
const bucket = new S3Bucket({ name: "my-bucket" });
|
|
176
|
+
const ref = bucket["name"];
|
|
177
|
+
`;
|
|
178
|
+
expect(foldConst(src, "ref")).toEqual({ __attrRef: { entity: "bucket", attribute: "name" } });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("numeric element access on a const bound to `new` becomes a symbolic ref with a stringified attribute", () => {
|
|
182
|
+
const src = `
|
|
183
|
+
const thing = new Thing({});
|
|
184
|
+
const ref = thing[0];
|
|
185
|
+
`;
|
|
186
|
+
expect(foldConst(src, "ref")).toEqual({ __attrRef: { entity: "thing", attribute: "0" } });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("a dynamic/computed element key throws a located FoldError (EVL003 semantics)", () => {
|
|
190
|
+
const src = `
|
|
191
|
+
const key = "dynamic";
|
|
192
|
+
const obj = { dynamic: 1 };
|
|
193
|
+
const bad = obj[key];
|
|
194
|
+
`;
|
|
195
|
+
let error: unknown;
|
|
196
|
+
try {
|
|
197
|
+
foldConst(src, "bad");
|
|
198
|
+
} catch (e) {
|
|
199
|
+
error = e;
|
|
200
|
+
}
|
|
201
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
202
|
+
expect((error as FoldError).message).toContain("computed key must be a string or numeric literal");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("a dynamic element key on a resource ref is also rejected", () => {
|
|
206
|
+
const src = `
|
|
207
|
+
const key = "arn";
|
|
208
|
+
const bucket = new S3Bucket({});
|
|
209
|
+
const bad = bucket[key];
|
|
210
|
+
`;
|
|
211
|
+
expect(() => foldConst(src, "bad")).toThrow(FoldError);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("fold — intrinsic tagged templates", () => {
|
|
216
|
+
const SUB: IntrinsicDef = { name: "Sub", isTag: true, outputKey: "Fn::Sub" };
|
|
217
|
+
|
|
218
|
+
test("a registered tag folds to the intrinsic node form", () => {
|
|
219
|
+
const src = `const x = Sub\`\${name}-data\`;`;
|
|
220
|
+
const consts = parseConsts(`const name = "prefix"; ${src}`);
|
|
221
|
+
const expr = consts.get("x");
|
|
222
|
+
if (!expr) throw new Error("fixture error");
|
|
223
|
+
expect(fold(expr, consts, [SUB])).toEqual({
|
|
224
|
+
__intrinsic: "Sub",
|
|
225
|
+
strings: ["", "-data"],
|
|
226
|
+
values: ["prefix"],
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("Params.StackName-style pseudo-parameter access is preserved symbolically, not stringified", () => {
|
|
231
|
+
const consts = parseConsts(`const x = Intrinsic\`\${Params.StackName}-data\`;`);
|
|
232
|
+
const expr = consts.get("x");
|
|
233
|
+
if (!expr) throw new Error("fixture error");
|
|
234
|
+
const INTRINSIC: IntrinsicDef = { name: "Intrinsic", isTag: true };
|
|
235
|
+
expect(fold(expr, consts, [INTRINSIC])).toEqual({
|
|
236
|
+
__intrinsic: "Intrinsic",
|
|
237
|
+
strings: ["", "-data"],
|
|
238
|
+
values: [{ __symbol: "Params.StackName" }],
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("folds only the interpolated sub-expressions — a nested resource ref stays a ref, not a string", () => {
|
|
243
|
+
const src = `
|
|
244
|
+
const bucket = new S3Bucket({ name: "my-bucket" });
|
|
245
|
+
const x = Sub\`\${bucket.arn}/*\`;
|
|
246
|
+
`;
|
|
247
|
+
const consts = parseConsts(src);
|
|
248
|
+
const expr = consts.get("x");
|
|
249
|
+
if (!expr) throw new Error("fixture error");
|
|
250
|
+
expect(fold(expr, consts, [SUB])).toEqual({
|
|
251
|
+
__intrinsic: "Sub",
|
|
252
|
+
strings: ["", "/*"],
|
|
253
|
+
values: [{ __attrRef: { entity: "bucket", attribute: "arn" } }],
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a no-substitution tagged template folds with an empty values array", () => {
|
|
258
|
+
const consts = parseConsts("const x = Sub`plain-value`;");
|
|
259
|
+
const expr = consts.get("x");
|
|
260
|
+
if (!expr) throw new Error("fixture error");
|
|
261
|
+
expect(fold(expr, consts, [SUB])).toEqual({ __intrinsic: "Sub", strings: ["plain-value"], values: [] });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("an unregistered tagged template is rejected with a located FoldError", () => {
|
|
265
|
+
const consts = parseConsts("const x = Unknown`${1}`;");
|
|
266
|
+
const expr = consts.get("x");
|
|
267
|
+
if (!expr) throw new Error("fixture error");
|
|
268
|
+
let error: unknown;
|
|
269
|
+
try {
|
|
270
|
+
fold(expr, consts, [SUB]);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
error = e;
|
|
273
|
+
}
|
|
274
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
275
|
+
expect((error as FoldError).message).toContain("Unknown");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("a tagged template is rejected when no intrinsics are registered at all", () => {
|
|
279
|
+
const consts = parseConsts("const x = Sub`${1}`;");
|
|
280
|
+
const expr = consts.get("x");
|
|
281
|
+
if (!expr) throw new Error("fixture error");
|
|
282
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* chant #1044 — registered lexicon intrinsics in PLAIN-CALL form.
|
|
288
|
+
*
|
|
289
|
+
* The registry is the whole door: a name folds as a call only when an active
|
|
290
|
+
* lexicon registered it AND set `foldsAsCall`. Everything below that pairs a
|
|
291
|
+
* folding case with the near-miss that must keep rejecting.
|
|
292
|
+
*/
|
|
293
|
+
describe("fold — registered call-form intrinsics (#1044)", () => {
|
|
294
|
+
/** Shaped like aws's real registration (lexicons/aws/src/plugin.ts). */
|
|
295
|
+
const REF: IntrinsicDef = { name: "Ref", isTag: false, foldsAsCall: true };
|
|
296
|
+
const JOIN: IntrinsicDef = { name: "Join", isTag: false, foldsAsCall: true };
|
|
297
|
+
/** Registered, but its lexicon never opted the call form in — the default. */
|
|
298
|
+
const NOT_OPTED_IN: IntrinsicDef = { name: "Reference", isTag: false };
|
|
299
|
+
const SUB: IntrinsicDef = { name: "Sub", isTag: true };
|
|
300
|
+
|
|
301
|
+
test("an opted-in call folds to the intrinsic node form, arguments folded in order", () => {
|
|
302
|
+
const consts = parseConsts(`const env = "prod"; const x = Ref(env);`);
|
|
303
|
+
const expr = consts.get("x");
|
|
304
|
+
if (!expr) throw new Error("fixture error");
|
|
305
|
+
expect(fold(expr, consts, [REF])).toEqual({ __intrinsic: "Ref", args: ["prod"] });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("the envelope is the same family the tagged-template form produces — same __intrinsic key, positional args", () => {
|
|
309
|
+
const consts = parseConsts(`const x = Join("-", ["a", "b"]);`);
|
|
310
|
+
const expr = consts.get("x");
|
|
311
|
+
if (!expr) throw new Error("fixture error");
|
|
312
|
+
expect(fold(expr, consts, [JOIN])).toEqual({ __intrinsic: "Join", args: ["-", ["a", "b"]] });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("calls nest, and a call-form intrinsic folds inside a registered tag's interpolation", () => {
|
|
316
|
+
const consts = parseConsts(`const env = "prod"; const x = Sub\`\${Ref(env)}-fn\`;`);
|
|
317
|
+
const expr = consts.get("x");
|
|
318
|
+
if (!expr) throw new Error("fixture error");
|
|
319
|
+
expect(fold(expr, consts, [SUB, REF])).toEqual({
|
|
320
|
+
__intrinsic: "Sub",
|
|
321
|
+
strings: ["", "-fn"],
|
|
322
|
+
values: [{ __intrinsic: "Ref", args: ["prod"] }],
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("an argument nothing can resolve stays symbolic, exactly as inside a tag", () => {
|
|
327
|
+
const consts = parseConsts(`const x = Ref(AWS.StackName);`);
|
|
328
|
+
const expr = consts.get("x");
|
|
329
|
+
if (!expr) throw new Error("fixture error");
|
|
330
|
+
expect(fold(expr, consts, [REF])).toEqual({
|
|
331
|
+
__intrinsic: "Ref",
|
|
332
|
+
args: [{ __symbol: "AWS.StackName" }],
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("an already-resolved cross-file binding wins over the symbolic path — identity must survive", () => {
|
|
337
|
+
const consts = parseConsts(`const x = Ref(environment);`);
|
|
338
|
+
const expr = consts.get("x");
|
|
339
|
+
if (!expr) throw new Error("fixture error");
|
|
340
|
+
const live = { marker: "the real, shared Parameter instance" };
|
|
341
|
+
const folded = fold(expr, consts, [REF], new Map([["environment", live]])) as {
|
|
342
|
+
__intrinsic: string;
|
|
343
|
+
args: unknown[];
|
|
344
|
+
};
|
|
345
|
+
expect(folded.args[0]).toBe(live);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("a REGISTERED intrinsic that was never opted in is still rejected — the opt-in is the door, not registration", () => {
|
|
349
|
+
const consts = parseConsts(`const x = Reference("db");`);
|
|
350
|
+
const expr = consts.get("x");
|
|
351
|
+
if (!expr) throw new Error("fixture error");
|
|
352
|
+
let error: unknown;
|
|
353
|
+
try {
|
|
354
|
+
fold(expr, consts, [NOT_OPTED_IN]);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
error = e;
|
|
357
|
+
}
|
|
358
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
359
|
+
expect((error as FoldError).message).toContain("function call as a value is not foldable: Reference(...)");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("an opted-in name is rejected when its lexicon isn't among the active ones", () => {
|
|
363
|
+
const consts = parseConsts(`const x = Ref("db");`);
|
|
364
|
+
const expr = consts.get("x");
|
|
365
|
+
if (!expr) throw new Error("fixture error");
|
|
366
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("a TAGGED-TEMPLATE intrinsic called as a plain function is rejected — the two forms are disjoint", () => {
|
|
370
|
+
const consts = parseConsts(`const x = Sub("literal");`);
|
|
371
|
+
const expr = consts.get("x");
|
|
372
|
+
if (!expr) throw new Error("fixture error");
|
|
373
|
+
expect(() => fold(expr, consts, [SUB])).toThrow(FoldError);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("a call-form intrinsic used as a TAG is rejected too — opting the call in doesn't register a tag", () => {
|
|
377
|
+
const consts = parseConsts(`const x = Ref\`literal\`;`);
|
|
378
|
+
const expr = consts.get("x");
|
|
379
|
+
if (!expr) throw new Error("fixture error");
|
|
380
|
+
let error: unknown;
|
|
381
|
+
try {
|
|
382
|
+
fold(expr, consts, [REF]);
|
|
383
|
+
} catch (e) {
|
|
384
|
+
error = e;
|
|
385
|
+
}
|
|
386
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
387
|
+
expect((error as FoldError).message).toContain("unregistered tagged template intrinsic: Ref");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("a registered name reached as a METHOD (`ns.Ref(...)`) is rejected — only a bare identifier callee folds", () => {
|
|
391
|
+
const consts = parseConsts(`const x = aws.Ref("db");`);
|
|
392
|
+
const expr = consts.get("x");
|
|
393
|
+
if (!expr) throw new Error("fixture error");
|
|
394
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("an array method taking a closure is rejected — .map is arbitrary JS, not a registered intrinsic", () => {
|
|
398
|
+
const consts = parseConsts(`const cidrs = ["10.0.0.0/24"]; const x = cidrs.map((c) => Ref(c));`);
|
|
399
|
+
const expr = consts.get("x");
|
|
400
|
+
if (!expr) throw new Error("fixture error");
|
|
401
|
+
let error: unknown;
|
|
402
|
+
try {
|
|
403
|
+
fold(expr, consts, [REF]);
|
|
404
|
+
} catch (e) {
|
|
405
|
+
error = e;
|
|
406
|
+
}
|
|
407
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
408
|
+
expect((error as FoldError).message).toContain("cidrs.map(...)");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("a user-defined function is rejected however it's named", () => {
|
|
412
|
+
const consts = parseConsts(`const x = makeName("a", "b");`);
|
|
413
|
+
const expr = consts.get("x");
|
|
414
|
+
if (!expr) throw new Error("fixture error");
|
|
415
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("a registered name SHADOWED by a local const is rejected — the local binding wins, and it isn't the lexicon's", () => {
|
|
419
|
+
const consts = parseConsts(`const Ref = (n) => ({ mine: n }); const x = Ref("db");`);
|
|
420
|
+
const expr = consts.get("x");
|
|
421
|
+
if (!expr) throw new Error("fixture error");
|
|
422
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("an unfoldable argument still rejects the whole call", () => {
|
|
426
|
+
const consts = parseConsts(`const x = Ref(getName());`);
|
|
427
|
+
const expr = consts.get("x");
|
|
428
|
+
if (!expr) throw new Error("fixture error");
|
|
429
|
+
let error: unknown;
|
|
430
|
+
try {
|
|
431
|
+
fold(expr, consts, [REF]);
|
|
432
|
+
} catch (e) {
|
|
433
|
+
error = e;
|
|
434
|
+
}
|
|
435
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
436
|
+
expect((error as FoldError).message).toContain("getName(...)");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("a same-file resource passed to an intrinsic call rejects rather than folding to a look-alike envelope", () => {
|
|
440
|
+
const consts = parseConsts(`const bucket = new S3Bucket({ name: "b" }); const x = Ref(bucket);`);
|
|
441
|
+
const expr = consts.get("x");
|
|
442
|
+
if (!expr) throw new Error("fixture error");
|
|
443
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("fold — registered authoring helpers (#1082)", () => {
|
|
448
|
+
test("a registered helper call folds to the helper-call node form, arguments folded in order", () => {
|
|
449
|
+
const consts = parseConsts(`const stack = "web"; const x = phase("Apply", [{ kind: "cfn-deploy", stack }]);`);
|
|
450
|
+
const expr = consts.get("x");
|
|
451
|
+
if (!expr) throw new Error("fixture error");
|
|
452
|
+
expect(fold(expr, consts)).toEqual({
|
|
453
|
+
__helper: "phase",
|
|
454
|
+
args: ["Apply", [{ kind: "cfn-deploy", stack: "web" }]],
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("helper calls nest, so a fan-out phase folds to nested envelopes", () => {
|
|
459
|
+
const consts = parseConsts(`const x = phase("Outer", [phase("Inner", []), gate("approve")]);`);
|
|
460
|
+
const expr = consts.get("x");
|
|
461
|
+
if (!expr) throw new Error("fixture error");
|
|
462
|
+
expect(fold(expr, consts)).toEqual({
|
|
463
|
+
__helper: "phase",
|
|
464
|
+
args: [
|
|
465
|
+
"Outer",
|
|
466
|
+
[
|
|
467
|
+
{ __helper: "phase", args: ["Inner", []] },
|
|
468
|
+
{ __helper: "gate", args: ["approve"] },
|
|
469
|
+
],
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("an unfoldable argument still rejects the whole call", () => {
|
|
475
|
+
const consts = parseConsts(`const x = phase("Apply", [{ stack: getName() }]);`);
|
|
476
|
+
const expr = consts.get("x");
|
|
477
|
+
if (!expr) throw new Error("fixture error");
|
|
478
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("an UNREGISTERED name is still rejected — the allowlist is closed, not a general call case", () => {
|
|
482
|
+
const consts = parseConsts(`const x = compose("Apply", []);`);
|
|
483
|
+
const expr = consts.get("x");
|
|
484
|
+
if (!expr) throw new Error("fixture error");
|
|
485
|
+
let error: unknown;
|
|
486
|
+
try {
|
|
487
|
+
fold(expr, consts);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
error = e;
|
|
490
|
+
}
|
|
491
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
492
|
+
expect((error as FoldError).message).toContain("function call as a value is not foldable: compose(...)");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("a registered name SHADOWED by a local const is rejected — the local binding wins, and it isn't chant's", () => {
|
|
496
|
+
const consts = parseConsts(`const phase = (n) => ({ phase: n }); const x = phase("Apply");`);
|
|
497
|
+
const expr = consts.get("x");
|
|
498
|
+
if (!expr) throw new Error("fixture error");
|
|
499
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("a registered name used as a METHOD (`ns.phase(...)`) is rejected — only a bare identifier callee folds", () => {
|
|
503
|
+
const consts = parseConsts(`const x = helpers.phase("Apply", []);`);
|
|
504
|
+
const expr = consts.get("x");
|
|
505
|
+
if (!expr) throw new Error("fixture error");
|
|
506
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("a spread argument is rejected — fold has no case for one, registered callee or not", () => {
|
|
510
|
+
const consts = parseConsts(`const steps = []; const x = phase(...steps);`);
|
|
511
|
+
const expr = consts.get("x");
|
|
512
|
+
if (!expr) throw new Error("fixture error");
|
|
513
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe("fold — nullish coalescing, ternary, arithmetic", () => {
|
|
518
|
+
test("?? falls through only on null/undefined", () => {
|
|
519
|
+
expect(foldConst(`const x = null ?? "fallback";`, "x")).toBe("fallback");
|
|
520
|
+
expect(foldConst(`const x = undefined ?? "fallback";`, "x")).toBe("fallback");
|
|
521
|
+
expect(foldConst(`const x = 0 ?? "fallback";`, "x")).toBe(0);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("ternary picks a branch", () => {
|
|
525
|
+
expect(foldConst(`const x = 2 > 1 ? "yes" : "no";`, "x")).toBe("yes");
|
|
526
|
+
expect(foldConst(`const x = 1 > 2 ? "yes" : "no";`, "x")).toBe("no");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("arithmetic operators", () => {
|
|
530
|
+
expect(foldConst(`const x = 2 + 3;`, "x")).toBe(5);
|
|
531
|
+
expect(foldConst(`const x = 5 - 2;`, "x")).toBe(3);
|
|
532
|
+
expect(foldConst(`const x = 4 * 3;`, "x")).toBe(12);
|
|
533
|
+
expect(foldConst(`const x = 9 / 3;`, "x")).toBe(3);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("+ concatenates strings", () => {
|
|
537
|
+
expect(foldConst(`const p = "fountain"; const x = p + "-" + "db";`, "x")).toBe("fountain-db");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("comparison operators", () => {
|
|
541
|
+
expect(foldConst(`const x = 3 === 3;`, "x")).toBe(true);
|
|
542
|
+
expect(foldConst(`const x = 3 !== 3;`, "x")).toBe(false);
|
|
543
|
+
expect(foldConst(`const x = 3 >= 3;`, "x")).toBe(true);
|
|
544
|
+
expect(foldConst(`const x = 2 <= 1;`, "x")).toBe(false);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
describe("fold — logical short-circuit (no execution of the untaken branch)", () => {
|
|
549
|
+
test("&& short-circuits on a falsy left without evaluating the call on the right", () => {
|
|
550
|
+
expect(foldConst(`const x = false && sideEffect();`, "x")).toBe(false);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("&& evaluates the right side when the left is truthy", () => {
|
|
554
|
+
expect(foldConst(`const x = true && "right";`, "x")).toBe("right");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("|| short-circuits on a truthy left without evaluating the call on the right", () => {
|
|
558
|
+
expect(foldConst(`const x = true || sideEffect();`, "x")).toBe(true);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("|| evaluates the right side when the left is falsy", () => {
|
|
562
|
+
expect(foldConst(`const x = false || "right";`, "x")).toBe("right");
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("fold — unary operators", () => {
|
|
567
|
+
test("logical not", () => {
|
|
568
|
+
expect(foldConst(`const x = !true;`, "x")).toBe(false);
|
|
569
|
+
expect(foldConst(`const x = !false;`, "x")).toBe(true);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("numeric negation", () => {
|
|
573
|
+
expect(foldConst(`const n = 5; const x = -n;`, "x")).toBe(-5);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe("fold — as / satisfies / parenthesized", () => {
|
|
578
|
+
test("`as` cast unwraps to the inner value", () => {
|
|
579
|
+
expect(foldConst(`const x = (1 + 1) as number;`, "x")).toBe(2);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("`satisfies` unwraps to the inner value", () => {
|
|
583
|
+
expect(foldConst(`const x = "ok" satisfies string;`, "x")).toBe("ok");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("parenthesized expressions unwrap", () => {
|
|
587
|
+
expect(foldConst(`const x = (("nested"));`, "x")).toBe("nested");
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("fold — rejections", () => {
|
|
592
|
+
test("a function call as a value throws FoldError naming the callee", () => {
|
|
593
|
+
const src = `const bad = computeName("x");`;
|
|
594
|
+
let error: unknown;
|
|
595
|
+
try {
|
|
596
|
+
foldConst(src, "bad");
|
|
597
|
+
} catch (e) {
|
|
598
|
+
error = e;
|
|
599
|
+
}
|
|
600
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
601
|
+
expect((error as FoldError).message).toContain("computeName(...)");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("a call throws FoldError with a located line/column", () => {
|
|
605
|
+
const src = `\nconst bad = computeName("x");`;
|
|
606
|
+
try {
|
|
607
|
+
foldConst(src, "bad");
|
|
608
|
+
throw new Error("expected foldConst to throw");
|
|
609
|
+
} catch (e) {
|
|
610
|
+
expect(e).toBeInstanceOf(FoldError);
|
|
611
|
+
const err = e as FoldError;
|
|
612
|
+
expect(err.line).toBe(2);
|
|
613
|
+
expect(err.column).toBeGreaterThan(0);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("a computed/dynamic property key is not foldable", () => {
|
|
618
|
+
const src = `
|
|
619
|
+
const key = "dynamic";
|
|
620
|
+
const bad = { [key]: 1 };
|
|
621
|
+
`;
|
|
622
|
+
expect(() => foldConst(src, "bad")).toThrow(FoldError);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("spread from a call expression is not foldable", () => {
|
|
626
|
+
const src = `const bad = { ...computeExtras() };`;
|
|
627
|
+
expect(() => foldConst(src, "bad")).toThrow(FoldError);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("spread from a resolved non-object value is not foldable", () => {
|
|
631
|
+
const src = `
|
|
632
|
+
const n = 5;
|
|
633
|
+
const bad = { ...n };
|
|
634
|
+
`;
|
|
635
|
+
expect(() => foldConst(src, "bad")).toThrow(FoldError);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("array-spread from a resolved non-array value is not foldable", () => {
|
|
639
|
+
const src = `
|
|
640
|
+
const n = 5;
|
|
641
|
+
const bad = [...n];
|
|
642
|
+
`;
|
|
643
|
+
expect(() => foldConst(src, "bad")).toThrow(FoldError);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("an unresolved identifier is not foldable", () => {
|
|
647
|
+
const src = `const bad = missingVar;`;
|
|
648
|
+
expect(() => foldConst(src, "bad")).toThrow(FoldError);
|
|
649
|
+
try {
|
|
650
|
+
foldConst(src, "bad");
|
|
651
|
+
} catch (e) {
|
|
652
|
+
expect((e as FoldError).message).toContain("missingVar");
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("an unsupported expression form is not foldable", () => {
|
|
657
|
+
const src = `const bad = function () { return 1; };`;
|
|
658
|
+
expect(() => foldConst(src, "bad")).toThrow(FoldError);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
describe("foldResource — constructor argument positions (#1082)", () => {
|
|
663
|
+
test("a props object that isn't the FIRST argument still folds, reported as a positional arg list", () => {
|
|
664
|
+
// AWS's deploy-time Parameter is `(type, props)` — lexicons/aws/src/parameter.ts.
|
|
665
|
+
const src = `export const p = new Parameter("String", { description: "vpc id", defaultValue: "" });`;
|
|
666
|
+
const result = foldModule(src);
|
|
667
|
+
expect(result.p).toEqual({
|
|
668
|
+
ok: true,
|
|
669
|
+
spec: {
|
|
670
|
+
__resource: "Parameter",
|
|
671
|
+
props: { description: "vpc id", defaultValue: "" },
|
|
672
|
+
args: ["String", { description: "vpc id", defaultValue: "" }],
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("a constructor with no object-literal argument at all folds to its bare argument list", () => {
|
|
678
|
+
const src = `export const p = new Parameter("String");`;
|
|
679
|
+
const result = foldModule(src);
|
|
680
|
+
expect(result.p).toEqual({ ok: true, spec: { __resource: "Parameter", props: {}, args: ["String"] } });
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("the classic (props) shape is unchanged — no `args` list", () => {
|
|
684
|
+
const src = `export const b = new S3Bucket({ name: "b" });`;
|
|
685
|
+
const result = foldModule(src);
|
|
686
|
+
expect(result.b).toEqual({ ok: true, spec: { __resource: "S3Bucket", props: { name: "b" } } });
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("the classic (props, attributes) shape is unchanged — no `args` list", () => {
|
|
690
|
+
const src = `export const b = new S3Bucket({ name: "b" }, { DependsOn: "other" });`;
|
|
691
|
+
const result = foldModule(src);
|
|
692
|
+
expect(result.b).toEqual({
|
|
693
|
+
ok: true,
|
|
694
|
+
spec: { __resource: "S3Bucket", props: { name: "b" }, attributes: { DependsOn: "other" } },
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("a non-object-literal argument is folded, not rejected for its position — but must still be foldable", () => {
|
|
699
|
+
const src = `export const p = new Parameter(getType(), { description: "x" });`;
|
|
700
|
+
const result = foldModule(src);
|
|
701
|
+
expect(result.p?.ok).toBe(false);
|
|
702
|
+
if (result.p && !result.p.ok) {
|
|
703
|
+
expect(result.p.error).toContain("getType(...)");
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
describe("foldModule", () => {
|
|
709
|
+
test("folds an exported const resource to { ok: true, spec }", () => {
|
|
710
|
+
const src = `
|
|
711
|
+
export const bucket = new S3Bucket({ name: "my-bucket", versioned: true });
|
|
712
|
+
`;
|
|
713
|
+
const result = foldModule(src);
|
|
714
|
+
expect(result.bucket).toEqual({
|
|
715
|
+
ok: true,
|
|
716
|
+
spec: { __resource: "S3Bucket", props: { name: "my-bucket", versioned: true } },
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("folds multiple resources, mixing ok and error results", () => {
|
|
721
|
+
const src = `
|
|
722
|
+
export const good = new Thing({ value: "ok" });
|
|
723
|
+
export const bad = new Thing({ value: computeName("x") });
|
|
724
|
+
`;
|
|
725
|
+
const result = foldModule(src);
|
|
726
|
+
|
|
727
|
+
expect(result.good).toEqual({ ok: true, spec: { __resource: "Thing", props: { value: "ok" } } });
|
|
728
|
+
expect(result.bad?.ok).toBe(false);
|
|
729
|
+
if (result.bad && !result.bad.ok) {
|
|
730
|
+
expect(result.bad.error).toContain("computeName(...)");
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test("resolves cross-resource attribute refs inside a folded resource", () => {
|
|
735
|
+
const src = `
|
|
736
|
+
export const bucket = new S3Bucket({ name: "my-bucket" });
|
|
737
|
+
export const policy = new BucketPolicy({ bucketName: bucket.name });
|
|
738
|
+
`;
|
|
739
|
+
const result = foldModule(src);
|
|
740
|
+
expect(result.policy).toEqual({
|
|
741
|
+
ok: true,
|
|
742
|
+
spec: {
|
|
743
|
+
__resource: "BucketPolicy",
|
|
744
|
+
props: { bucketName: { __attrRef: { entity: "bucket", attribute: "name" } } },
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("leaves non-resource const exports out of the result", () => {
|
|
750
|
+
const src = `
|
|
751
|
+
export const helperValue = 5;
|
|
752
|
+
export const thing = new Thing({ value: "ok" });
|
|
753
|
+
`;
|
|
754
|
+
const result = foldModule(src);
|
|
755
|
+
expect(result.thing).toBeDefined();
|
|
756
|
+
expect(result.helperValue).toBeUndefined();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("performs zero module execution — a throwing top-level statement never runs", () => {
|
|
760
|
+
const src = `
|
|
761
|
+
throw new Error("this must never execute");
|
|
762
|
+
export const thing = new Thing({ value: "ok" });
|
|
763
|
+
`;
|
|
764
|
+
// If foldModule ran the module, this call itself would throw.
|
|
765
|
+
const result = foldModule(src);
|
|
766
|
+
expect(result.thing).toEqual({ ok: true, spec: { __resource: "Thing", props: { value: "ok" } } });
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// #1025 differential regression: `new Type(props, attributes)`'s second
|
|
770
|
+
// argument (CFN-style resource attributes — DependsOn, Condition,
|
|
771
|
+
// DeletionPolicy, UpdateReplacePolicy, CreationPolicy, Metadata — see
|
|
772
|
+
// `createResource`'s `attributes` param in ../runtime.ts) was silently
|
|
773
|
+
// dropped by `foldResource`, which only ever read `node.arguments[0]`. The
|
|
774
|
+
// #1025 fold-vs-run differential corpus caught this as real drift on
|
|
775
|
+
// `lexicons/aws/examples/docs-snippets` (its `resource-attributes.ts` and
|
|
776
|
+
// `depends-on.ts` snippets both pass a second argument) before this fix.
|
|
777
|
+
test("folds the optional second constructor argument as `attributes`", () => {
|
|
778
|
+
const src = `
|
|
779
|
+
export const dbInstance = new DbInstance(
|
|
780
|
+
{ Engine: "postgres" },
|
|
781
|
+
{ DeletionPolicy: "Snapshot", UpdateReplacePolicy: "Snapshot" },
|
|
782
|
+
);
|
|
783
|
+
`;
|
|
784
|
+
const result = foldModule(src);
|
|
785
|
+
expect(result.dbInstance).toEqual({
|
|
786
|
+
ok: true,
|
|
787
|
+
spec: {
|
|
788
|
+
__resource: "DbInstance",
|
|
789
|
+
props: { Engine: "postgres" },
|
|
790
|
+
attributes: { DeletionPolicy: "Snapshot", UpdateReplacePolicy: "Snapshot" },
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("omits `attributes` from the spec when the constructor takes only props", () => {
|
|
796
|
+
const src = `export const bucket = new S3Bucket({ name: "my-bucket" });`;
|
|
797
|
+
const result = foldModule(src);
|
|
798
|
+
expect(result.bucket?.ok).toBe(true);
|
|
799
|
+
if (result.bucket?.ok) {
|
|
800
|
+
expect(result.bucket.spec.attributes).toBeUndefined();
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("a non-object-literal second argument is not foldable", () => {
|
|
805
|
+
const src = `
|
|
806
|
+
const attrs = computeAttrs();
|
|
807
|
+
export const bad = new Thing({ value: "ok" }, attrs);
|
|
808
|
+
`;
|
|
809
|
+
const result = foldModule(src);
|
|
810
|
+
expect(result.bad?.ok).toBe(false);
|
|
811
|
+
});
|
|
812
|
+
});
|