@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,1598 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { join, dirname, resolve } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { tryFoldFile, createFoldSession } from "./fold-import";
|
|
7
|
+
import { isDeclarable } from "../declarable";
|
|
8
|
+
import { isCompositeInstance } from "../composite";
|
|
9
|
+
import { isAttrRefLike } from "../utils";
|
|
10
|
+
import { params as sharedParams } from "../params";
|
|
11
|
+
import type { AttrRef } from "../attrref";
|
|
12
|
+
import type { IntrinsicDef } from "../lexicon";
|
|
13
|
+
|
|
14
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
/** Absolute path to `packages/core/src/runtime.ts` — the real `createResource`
|
|
16
|
+
* factory, imported by fixture files below exactly as a lexicon package
|
|
17
|
+
* would be imported by real chant source. */
|
|
18
|
+
const runtimePath = resolve(thisDir, "../runtime");
|
|
19
|
+
/** Absolute path to `packages/core/src/composite.ts` — the real `Composite`
|
|
20
|
+
* factory, used to build a genuine composite fixture. */
|
|
21
|
+
const compositePath = resolve(thisDir, "../composite");
|
|
22
|
+
/** Absolute path to `packages/core/src/intrinsic.ts` — the real `Intrinsic`
|
|
23
|
+
* marker/interface every lexicon's own intrinsic classes (AWS's `Sub`,
|
|
24
|
+
* gitlab's `reference`, …) implement. Used below to build a tagged-template
|
|
25
|
+
* intrinsic fixture that is structurally identical to a real lexicon's,
|
|
26
|
+
* not a simplified stand-in. */
|
|
27
|
+
const intrinsicPath = resolve(thisDir, "../intrinsic");
|
|
28
|
+
/** Absolute path to `packages/core/src/params.ts` — chant #1064's build-time-parameters
|
|
29
|
+
* runtime module. A real project imports this as the bare specifier
|
|
30
|
+
* "@intentius/chant/params"; fixture files below use the absolute path instead
|
|
31
|
+
* (same convention as `runtimePath` etc. above), but {@link buildExternals}'s
|
|
32
|
+
* recognition is specifier-shape-agnostic — see fold-import.ts's own comment —
|
|
33
|
+
* so this exercises the identical code path a real bare import would take. */
|
|
34
|
+
const paramsPath = resolve(thisDir, "../params");
|
|
35
|
+
|
|
36
|
+
describe("tryFoldFile", () => {
|
|
37
|
+
let testDir: string;
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
testDir = join(tmpdir(), `chant-fold-import-test-${Date.now()}-${Math.random()}`);
|
|
41
|
+
await mkdir(testDir, { recursive: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
await rm(testDir, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
async function writeResourceDefs(): Promise<void> {
|
|
49
|
+
await writeFile(
|
|
50
|
+
join(testDir, "resources.ts"),
|
|
51
|
+
`
|
|
52
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
53
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
54
|
+
`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test("folds a leaf resource to a real Declarable with zero module execution", async () => {
|
|
59
|
+
await writeResourceDefs();
|
|
60
|
+
const file = join(testDir, "main.ts");
|
|
61
|
+
await writeFile(
|
|
62
|
+
file,
|
|
63
|
+
`
|
|
64
|
+
import { Bucket } from "./resources";
|
|
65
|
+
throw new Error("must never execute — sentinel for #1022 fold verification");
|
|
66
|
+
export const bucket = new Bucket({ name: "my-bucket", versioned: true });
|
|
67
|
+
`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await tryFoldFile(file);
|
|
71
|
+
|
|
72
|
+
expect(result.ok).toBe(true);
|
|
73
|
+
if (!result.ok) return;
|
|
74
|
+
expect(result.entities).toHaveLength(1);
|
|
75
|
+
const [name, entity] = result.entities[0];
|
|
76
|
+
expect(name).toBe("bucket");
|
|
77
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
78
|
+
expect(entity.lexicon).toBe("aws");
|
|
79
|
+
expect(entity.entityType).toBe("Test::Bucket");
|
|
80
|
+
expect((entity as unknown as { props: unknown }).props).toEqual({
|
|
81
|
+
name: "my-bucket",
|
|
82
|
+
versioned: true,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("folds multiple leaf resources from one file", async () => {
|
|
87
|
+
await writeResourceDefs();
|
|
88
|
+
const file = join(testDir, "main.ts");
|
|
89
|
+
await writeFile(
|
|
90
|
+
file,
|
|
91
|
+
`
|
|
92
|
+
import { Bucket } from "./resources";
|
|
93
|
+
export const a = new Bucket({ name: "a" });
|
|
94
|
+
export const b = new Bucket({ name: "b" });
|
|
95
|
+
`,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const result = await tryFoldFile(file);
|
|
99
|
+
|
|
100
|
+
expect(result.ok).toBe(true);
|
|
101
|
+
if (!result.ok) return;
|
|
102
|
+
expect(result.entities.map(([name]) => name).sort()).toEqual(["a", "b"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// chant #1023 (epic #1019 Phase 5): a bare composite factory call, resolved
|
|
106
|
+
// through the file's own imports, now folds — the factory is invoked for
|
|
107
|
+
// real with statically-folded props, exactly as `#1022`'s resource
|
|
108
|
+
// constructors already were. This used to be the canonical fallback case;
|
|
109
|
+
// flipping it to fold is the point of #1023.
|
|
110
|
+
test("folds a top-level composite factory call, zero module execution", async () => {
|
|
111
|
+
await writeResourceDefs();
|
|
112
|
+
await writeFile(
|
|
113
|
+
join(testDir, "composites.ts"),
|
|
114
|
+
`
|
|
115
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
116
|
+
import { Composite } from ${JSON.stringify(compositePath)};
|
|
117
|
+
const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
118
|
+
export const MyStack = Composite<{ name: string }>((props) => {
|
|
119
|
+
const bucket = new Bucket({ name: props.name });
|
|
120
|
+
return { bucket };
|
|
121
|
+
}, "MyStack");
|
|
122
|
+
`,
|
|
123
|
+
);
|
|
124
|
+
const file = join(testDir, "stack.ts");
|
|
125
|
+
await writeFile(
|
|
126
|
+
file,
|
|
127
|
+
`
|
|
128
|
+
import { MyStack } from "./composites";
|
|
129
|
+
throw new Error("must never execute — sentinel for #1023 fold verification");
|
|
130
|
+
export const stack = MyStack({ name: "composite-bucket" });
|
|
131
|
+
`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = await tryFoldFile(file);
|
|
135
|
+
|
|
136
|
+
expect(result.ok).toBe(true);
|
|
137
|
+
if (!result.ok) return;
|
|
138
|
+
expect(result.entities).toHaveLength(1);
|
|
139
|
+
const [name, entity] = result.entities[0];
|
|
140
|
+
expect(name).toBe("stack");
|
|
141
|
+
expect(isCompositeInstance(entity)).toBe(true);
|
|
142
|
+
const bucket = (entity as unknown as { bucket: unknown }).bucket;
|
|
143
|
+
if (!isDeclarable(bucket)) throw new Error("expected the composite's `bucket` member to be a Declarable");
|
|
144
|
+
expect(bucket.entityType).toBe("Test::Bucket");
|
|
145
|
+
expect((bucket as unknown as { props: unknown }).props).toEqual({ name: "composite-bucket" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("falls back when the composite factory is defined locally (not resolvable via import)", async () => {
|
|
149
|
+
await writeResourceDefs();
|
|
150
|
+
const file = join(testDir, "stack.ts");
|
|
151
|
+
await writeFile(
|
|
152
|
+
file,
|
|
153
|
+
`
|
|
154
|
+
import { Bucket } from "./resources";
|
|
155
|
+
import { Composite } from ${JSON.stringify(compositePath)};
|
|
156
|
+
const LocalStack = Composite<{ name: string }>((props) => {
|
|
157
|
+
const bucket = new Bucket({ name: props.name });
|
|
158
|
+
return { bucket };
|
|
159
|
+
}, "LocalStack");
|
|
160
|
+
export const stack = LocalStack({ name: "composite-bucket" });
|
|
161
|
+
`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const result = await tryFoldFile(file);
|
|
165
|
+
|
|
166
|
+
expect(result.ok).toBe(false);
|
|
167
|
+
if (result.ok) return;
|
|
168
|
+
expect(result.reason).toContain("LocalStack");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("falls back when a prop value is not foldable", async () => {
|
|
172
|
+
await writeResourceDefs();
|
|
173
|
+
const file = join(testDir, "main.ts");
|
|
174
|
+
await writeFile(
|
|
175
|
+
file,
|
|
176
|
+
`
|
|
177
|
+
import { Bucket } from "./resources";
|
|
178
|
+
function computeName(): string { return "dynamic"; }
|
|
179
|
+
export const bucket = new Bucket({ name: computeName() });
|
|
180
|
+
`,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const result = await tryFoldFile(file);
|
|
184
|
+
|
|
185
|
+
expect(result.ok).toBe(false);
|
|
186
|
+
if (result.ok) return;
|
|
187
|
+
expect(result.reason).toContain("computeName");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("falls back when the resource constructor is not a resolvable import", async () => {
|
|
191
|
+
const file = join(testDir, "main.ts");
|
|
192
|
+
await writeFile(
|
|
193
|
+
file,
|
|
194
|
+
`
|
|
195
|
+
export const bucket = new Bucket({ name: "my-bucket" });
|
|
196
|
+
`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const result = await tryFoldFile(file);
|
|
200
|
+
|
|
201
|
+
expect(result.ok).toBe(false);
|
|
202
|
+
if (result.ok) return;
|
|
203
|
+
expect(result.reason).toContain("not a resolvable import");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("falls back on `export default`", async () => {
|
|
207
|
+
await writeResourceDefs();
|
|
208
|
+
const file = join(testDir, "main.ts");
|
|
209
|
+
await writeFile(
|
|
210
|
+
file,
|
|
211
|
+
`
|
|
212
|
+
import { Bucket } from "./resources";
|
|
213
|
+
const bucket = new Bucket({ name: "my-bucket" });
|
|
214
|
+
export default bucket;
|
|
215
|
+
`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const result = await tryFoldFile(file);
|
|
219
|
+
|
|
220
|
+
expect(result.ok).toBe(false);
|
|
221
|
+
if (result.ok) return;
|
|
222
|
+
expect(result.reason).toContain("export default");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("falls back when there are no exports at all", async () => {
|
|
226
|
+
const file = join(testDir, "main.ts");
|
|
227
|
+
await writeFile(file, `const helperValue = 5;`);
|
|
228
|
+
|
|
229
|
+
const result = await tryFoldFile(file);
|
|
230
|
+
|
|
231
|
+
expect(result.ok).toBe(false);
|
|
232
|
+
if (result.ok) return;
|
|
233
|
+
expect(result.reason).toBe("no foldable resource exports");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// chant #1020: a plain-value-only export now folds too (contributing
|
|
237
|
+
// nothing to `entities` — only Declarable/CompositeInstance land there —
|
|
238
|
+
// but recorded in `exportedValues`). This is intentional, not a relaxed
|
|
239
|
+
// subset: `fold()` already reduced a bare literal trivially; the only
|
|
240
|
+
// change is that `tryFoldFile` now ALSO tries it for a "single" declarator
|
|
241
|
+
// instead of only the composite-call spine. It matters for #1020's own
|
|
242
|
+
// acceptance criterion — a config/params file that exports nothing but
|
|
243
|
+
// plain consts must be safe to fold (0 entities either way), or
|
|
244
|
+
// `planFoldTaint` would needlessly force every file that imports one of
|
|
245
|
+
// its constants back to run too.
|
|
246
|
+
test("a plain-value-only export now folds — zero entities, but the value is recorded for cross-file reference", async () => {
|
|
247
|
+
const file = join(testDir, "main.ts");
|
|
248
|
+
await writeFile(file, `export const helperValue = 5;`);
|
|
249
|
+
|
|
250
|
+
const result = await tryFoldFile(file);
|
|
251
|
+
|
|
252
|
+
expect(result.ok).toBe(true);
|
|
253
|
+
if (!result.ok) return;
|
|
254
|
+
expect(result.entities).toEqual([]);
|
|
255
|
+
expect(result.exportedValues.get("helperValue")).toBe(5);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// #1025 differential regression: the constructor's optional second
|
|
259
|
+
// argument (CFN-style resource attributes — DependsOn, Condition,
|
|
260
|
+
// DeletionPolicy, …) was folded by fold.ts's `foldResource` but then
|
|
261
|
+
// dropped on the floor here — only `entry.spec.props` was ever passed to
|
|
262
|
+
// `new ResourceCtor(...)`. Confirms the fix actually reaches the
|
|
263
|
+
// constructed `Declarable`, not just the intermediate `FoldedResource`
|
|
264
|
+
// spec (see fold.test.ts for that narrower unit).
|
|
265
|
+
test("passes the folded second argument through as the constructed entity's attributes", async () => {
|
|
266
|
+
await writeResourceDefs();
|
|
267
|
+
const file = join(testDir, "main.ts");
|
|
268
|
+
await writeFile(
|
|
269
|
+
file,
|
|
270
|
+
`
|
|
271
|
+
import { Bucket } from "./resources";
|
|
272
|
+
export const bucket = new Bucket(
|
|
273
|
+
{ name: "my-bucket" },
|
|
274
|
+
{ DeletionPolicy: "Retain", DependsOn: "otherResource" },
|
|
275
|
+
);
|
|
276
|
+
`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const result = await tryFoldFile(file);
|
|
280
|
+
|
|
281
|
+
expect(result.ok).toBe(true);
|
|
282
|
+
if (!result.ok) return;
|
|
283
|
+
const [, entity] = result.entities[0];
|
|
284
|
+
expect((entity as unknown as { attributes: unknown }).attributes).toEqual({
|
|
285
|
+
DeletionPolicy: "Retain",
|
|
286
|
+
DependsOn: "otherResource",
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// chant #1020 (epic #1019) — cross-file resolution: an imported binding now
|
|
292
|
+
// folds by resolving it to its `export const` initializer and folding it in
|
|
293
|
+
// the DEFINING module's own scope, instead of failing the whole importing
|
|
294
|
+
// file on the first identifier it doesn't recognize.
|
|
295
|
+
describe("tryFoldFile — cross-file resolution (chant #1020)", () => {
|
|
296
|
+
let testDir: string;
|
|
297
|
+
|
|
298
|
+
beforeEach(async () => {
|
|
299
|
+
testDir = join(tmpdir(), `chant-fold-import-crossfile-test-${Date.now()}-${Math.random()}`);
|
|
300
|
+
await mkdir(testDir, { recursive: true });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
afterEach(async () => {
|
|
304
|
+
await rm(testDir, { recursive: true, force: true });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
async function writeResourceDefs(): Promise<void> {
|
|
308
|
+
await writeFile(
|
|
309
|
+
join(testDir, "resources.ts"),
|
|
310
|
+
`
|
|
311
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
312
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
313
|
+
export const Vpc = createResource("Test::Vpc", "aws", { vpcId: "VpcId" });
|
|
314
|
+
export const Alb = createResource("Test::Alb", "aws", { albArn: "AlbArn" });
|
|
315
|
+
`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
test("an imported plain const folds identically to an inline one", async () => {
|
|
320
|
+
await writeResourceDefs();
|
|
321
|
+
await writeFile(join(testDir, "config.ts"), `export const REGION = "us-east-1";`);
|
|
322
|
+
const file = join(testDir, "main.ts");
|
|
323
|
+
await writeFile(
|
|
324
|
+
file,
|
|
325
|
+
`
|
|
326
|
+
import { Bucket } from "./resources";
|
|
327
|
+
import { REGION } from "./config";
|
|
328
|
+
export const bucket = new Bucket({ name: "b", region: REGION });
|
|
329
|
+
`,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const result = await tryFoldFile(file);
|
|
333
|
+
|
|
334
|
+
expect(result.ok).toBe(true);
|
|
335
|
+
if (!result.ok) return;
|
|
336
|
+
const [, entity] = result.entities[0];
|
|
337
|
+
expect((entity as unknown as { props: unknown }).props).toEqual({ name: "b", region: "us-east-1" });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("config.ts's own fold succeeds with zero entities — a plain-const-only file must not be tainted merely for existing", async () => {
|
|
341
|
+
const file = join(testDir, "config.ts");
|
|
342
|
+
await writeFile(file, `export const REGION = "us-east-1";\nexport const RETRIES = 3;`);
|
|
343
|
+
|
|
344
|
+
const result = await tryFoldFile(file);
|
|
345
|
+
|
|
346
|
+
expect(result.ok).toBe(true);
|
|
347
|
+
if (!result.ok) return;
|
|
348
|
+
expect(result.entities).toEqual([]);
|
|
349
|
+
expect(result.exportedValues.get("REGION")).toBe("us-east-1");
|
|
350
|
+
expect(result.exportedValues.get("RETRIES")).toBe(3);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("a cross-file resource attribute reference folds to a real AttrRef", async () => {
|
|
354
|
+
await writeResourceDefs();
|
|
355
|
+
await writeFile(
|
|
356
|
+
join(testDir, "network.ts"),
|
|
357
|
+
`
|
|
358
|
+
import { Vpc } from "./resources";
|
|
359
|
+
export const vpc = new Vpc({ cidr: "10.0.0.0/16" });
|
|
360
|
+
`,
|
|
361
|
+
);
|
|
362
|
+
const file = join(testDir, "alb.ts");
|
|
363
|
+
await writeFile(
|
|
364
|
+
file,
|
|
365
|
+
`
|
|
366
|
+
import { Alb } from "./resources";
|
|
367
|
+
import { vpc } from "./network";
|
|
368
|
+
export const alb = new Alb({ vpcId: vpc.vpcId });
|
|
369
|
+
`,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const result = await tryFoldFile(file);
|
|
373
|
+
|
|
374
|
+
expect(result.ok).toBe(true);
|
|
375
|
+
if (!result.ok) return;
|
|
376
|
+
const [, entity] = result.entities[0];
|
|
377
|
+
const vpcId = (entity as unknown as { props: { vpcId: unknown } }).props.vpcId;
|
|
378
|
+
expect(isAttrRefLike(vpcId)).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// The hard part (see planFoldTaint's module doc): fold and run must never
|
|
382
|
+
// disagree about object identity. If `alb.ts` reached into `network.ts`
|
|
383
|
+
// and built a SECOND copy of `network.vpc`, that copy would never match
|
|
384
|
+
// discover()'s entities map by identity and serialization would crash, not
|
|
385
|
+
// drift. Asserted with `toBe` — identity, not `toEqual`'s structural check.
|
|
386
|
+
test("a cross-file reference shares the EXACT SAME entity instance as the defining file's own fold — not a duplicate", async () => {
|
|
387
|
+
await writeResourceDefs();
|
|
388
|
+
const networkFile = join(testDir, "network.ts");
|
|
389
|
+
await writeFile(
|
|
390
|
+
networkFile,
|
|
391
|
+
`
|
|
392
|
+
import { Vpc } from "./resources";
|
|
393
|
+
export const vpc = new Vpc({ cidr: "10.0.0.0/16" });
|
|
394
|
+
`,
|
|
395
|
+
);
|
|
396
|
+
const albFile = join(testDir, "alb.ts");
|
|
397
|
+
await writeFile(
|
|
398
|
+
albFile,
|
|
399
|
+
`
|
|
400
|
+
import { Alb } from "./resources";
|
|
401
|
+
import { vpc } from "./network";
|
|
402
|
+
export const alb = new Alb({ vpcId: vpc.vpcId });
|
|
403
|
+
`,
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// One shared session, exactly as `discover()` gives its own per-file
|
|
407
|
+
// loop — this is what a real build does; two independent `tryFoldFile`
|
|
408
|
+
// calls with no session would each fold `network.ts` on their own and
|
|
409
|
+
// legitimately produce two different (if structurally equal) objects.
|
|
410
|
+
const session = createFoldSession();
|
|
411
|
+
const networkResult = await tryFoldFile(networkFile, [], session);
|
|
412
|
+
const albResult = await tryFoldFile(albFile, [], session);
|
|
413
|
+
|
|
414
|
+
expect(networkResult.ok).toBe(true);
|
|
415
|
+
expect(albResult.ok).toBe(true);
|
|
416
|
+
if (!networkResult.ok || !albResult.ok) return;
|
|
417
|
+
|
|
418
|
+
const [, vpcEntity] = networkResult.entities[0];
|
|
419
|
+
const [, albEntity] = albResult.entities[0];
|
|
420
|
+
const vpcIdRef = (albEntity as unknown as { props: { vpcId: AttrRef } }).props.vpcId;
|
|
421
|
+
expect(vpcIdRef.parent.deref()).toBe(vpcEntity);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// A diamond — TWO paths reaching the SAME module (b.ts and c.ts both
|
|
425
|
+
// import hub.ts; neither imports the other) — is the case that silently
|
|
426
|
+
// degrades to duplicated (or, nested a few levels deep, EXPONENTIAL) work
|
|
427
|
+
// without a resolution memo, yet produces no error at all: each path just
|
|
428
|
+
// independently re-resolves the shared module. Distinct from the reference
|
|
429
|
+
// CYCLE tests below, which fail loudly; this one must instead prove the
|
|
430
|
+
// hub was folded exactly once and every referrer shares that one object.
|
|
431
|
+
// An explicit test timeout below turns a reintroduced non-terminating bug
|
|
432
|
+
// into a fast, clearly-labeled failure instead of a hung test run.
|
|
433
|
+
test(
|
|
434
|
+
"a diamond (two independent paths reaching the same module) resolves without re-resolving the shared module",
|
|
435
|
+
async () => {
|
|
436
|
+
await writeResourceDefs();
|
|
437
|
+
await writeFile(
|
|
438
|
+
join(testDir, "hub.ts"),
|
|
439
|
+
`
|
|
440
|
+
import { Vpc } from "./resources";
|
|
441
|
+
export const vpc = new Vpc({ cidr: "10.0.0.0/16" });
|
|
442
|
+
`,
|
|
443
|
+
);
|
|
444
|
+
await writeFile(
|
|
445
|
+
join(testDir, "b.ts"),
|
|
446
|
+
`
|
|
447
|
+
import { Alb } from "./resources";
|
|
448
|
+
import { vpc } from "./hub";
|
|
449
|
+
export const b = new Alb({ vpcId: vpc.vpcId });
|
|
450
|
+
`,
|
|
451
|
+
);
|
|
452
|
+
await writeFile(
|
|
453
|
+
join(testDir, "c.ts"),
|
|
454
|
+
`
|
|
455
|
+
import { Alb } from "./resources";
|
|
456
|
+
import { vpc } from "./hub";
|
|
457
|
+
export const c = new Alb({ vpcId: vpc.vpcId });
|
|
458
|
+
`,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const session = createFoldSession();
|
|
462
|
+
const hubResult = await tryFoldFile(join(testDir, "hub.ts"), [], session);
|
|
463
|
+
const bResult = await tryFoldFile(join(testDir, "b.ts"), [], session);
|
|
464
|
+
const cResult = await tryFoldFile(join(testDir, "c.ts"), [], session);
|
|
465
|
+
|
|
466
|
+
expect(hubResult.ok).toBe(true);
|
|
467
|
+
expect(bResult.ok).toBe(true);
|
|
468
|
+
expect(cResult.ok).toBe(true);
|
|
469
|
+
if (!hubResult.ok || !bResult.ok || !cResult.ok) return;
|
|
470
|
+
|
|
471
|
+
const [, hubVpc] = hubResult.entities[0];
|
|
472
|
+
const [, bEntity] = bResult.entities[0];
|
|
473
|
+
const [, cEntity] = cResult.entities[0];
|
|
474
|
+
const bVpcId = (bEntity as unknown as { props: { vpcId: AttrRef } }).props.vpcId;
|
|
475
|
+
const cVpcId = (cEntity as unknown as { props: { vpcId: AttrRef } }).props.vpcId;
|
|
476
|
+
// If the hub were re-resolved once per path (the un-memoized failure
|
|
477
|
+
// mode), each path would construct its OWN, distinct `Vpc` instance —
|
|
478
|
+
// these `toBe` checks would fail (not hang) in that case.
|
|
479
|
+
expect(bVpcId.parent.deref()).toBe(hubVpc);
|
|
480
|
+
expect(cVpcId.parent.deref()).toBe(hubVpc);
|
|
481
|
+
// hub.ts/b.ts/c.ts (3) plus the two shared fixture-support files each
|
|
482
|
+
// of them transitively imports — resources.ts, and (via its own
|
|
483
|
+
// absolute-path import, see `runtimePath`'s doc above) runtime.ts
|
|
484
|
+
// itself — each attempted/cached exactly ONCE too, not once per
|
|
485
|
+
// referrer. The point of this count isn't the literal number 5; it's
|
|
486
|
+
// that it's flat regardless of how many paths (b.ts AND c.ts) lead to
|
|
487
|
+
// the SAME file, which is what a broken/missing memo would violate.
|
|
488
|
+
expect(session.cache.size).toBe(5);
|
|
489
|
+
},
|
|
490
|
+
5000,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// A wider fan-out (many files, all reaching one shared hub, plus a short
|
|
494
|
+
// dependency chain) than the minimal diamond above — cheap enough to stay
|
|
495
|
+
// well under the explicit timeout if resolution is O(files), but would
|
|
496
|
+
// measurably blow up if the hub (or the chain) were re-resolved once per
|
|
497
|
+
// referrer instead of once per session.
|
|
498
|
+
test(
|
|
499
|
+
"a wide fan-out to one shared hub resolves promptly (no combinatorial blowup)",
|
|
500
|
+
async () => {
|
|
501
|
+
await writeResourceDefs();
|
|
502
|
+
await writeFile(
|
|
503
|
+
join(testDir, "hub.ts"),
|
|
504
|
+
`
|
|
505
|
+
import { Vpc } from "./resources";
|
|
506
|
+
export const vpc = new Vpc({ cidr: "10.0.0.0/16" });
|
|
507
|
+
`,
|
|
508
|
+
);
|
|
509
|
+
const FANOUT = 15;
|
|
510
|
+
const files: string[] = [];
|
|
511
|
+
for (let i = 0; i < FANOUT; i++) {
|
|
512
|
+
const f = join(testDir, `leaf${i}.ts`);
|
|
513
|
+
await writeFile(
|
|
514
|
+
f,
|
|
515
|
+
`
|
|
516
|
+
import { Alb } from "./resources";
|
|
517
|
+
import { vpc } from "./hub";
|
|
518
|
+
export const leaf = new Alb({ vpcId: vpc.vpcId });
|
|
519
|
+
`,
|
|
520
|
+
);
|
|
521
|
+
files.push(f);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const session = createFoldSession();
|
|
525
|
+
const hubResult = await tryFoldFile(join(testDir, "hub.ts"), [], session);
|
|
526
|
+
expect(hubResult.ok).toBe(true);
|
|
527
|
+
if (!hubResult.ok) return;
|
|
528
|
+
const [, hubVpc] = hubResult.entities[0];
|
|
529
|
+
|
|
530
|
+
for (const f of files) {
|
|
531
|
+
const result = await tryFoldFile(f, [], session);
|
|
532
|
+
expect(result.ok).toBe(true);
|
|
533
|
+
if (!result.ok) continue;
|
|
534
|
+
const [, entity] = result.entities[0];
|
|
535
|
+
const vpcId = (entity as unknown as { props: { vpcId: AttrRef } }).props.vpcId;
|
|
536
|
+
expect(vpcId.parent.deref()).toBe(hubVpc);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// hub.ts + the two shared fixture-support files (resources.ts,
|
|
540
|
+
// runtime.ts — see the diamond test above) + FANOUT leaves. Flat in
|
|
541
|
+
// FANOUT's shared dependencies regardless of FANOUT's size is exactly
|
|
542
|
+
// the property a broken memo would violate.
|
|
543
|
+
expect(session.cache.size).toBe(FANOUT + 3);
|
|
544
|
+
},
|
|
545
|
+
5000,
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
test("a namespace import's member access resolves cross-file (import * as ns)", async () => {
|
|
549
|
+
await writeResourceDefs();
|
|
550
|
+
await writeFile(
|
|
551
|
+
join(testDir, "ecr.ts"),
|
|
552
|
+
`
|
|
553
|
+
import { Bucket } from "./resources";
|
|
554
|
+
export const apiRepo = new Bucket({ name: "api-repo" });
|
|
555
|
+
export const uiRepo = new Bucket({ name: "ui-repo" });
|
|
556
|
+
`,
|
|
557
|
+
);
|
|
558
|
+
const file = join(testDir, "outputs.ts");
|
|
559
|
+
await writeFile(
|
|
560
|
+
file,
|
|
561
|
+
`
|
|
562
|
+
import { Alb } from "./resources";
|
|
563
|
+
import * as ecr from "./ecr";
|
|
564
|
+
export const alb = new Alb({ albArn: ecr.apiRepo.arn });
|
|
565
|
+
`,
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const result = await tryFoldFile(file);
|
|
569
|
+
|
|
570
|
+
expect(result.ok).toBe(true);
|
|
571
|
+
if (!result.ok) return;
|
|
572
|
+
const [, entity] = result.entities[0];
|
|
573
|
+
const albArn = (entity as unknown as { props: { albArn: unknown } }).props.albArn;
|
|
574
|
+
expect(isAttrRefLike(albArn)).toBe(true);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("a re-export chain (`export { x } from \"./y\"`) resolves cross-file, sharing the same instance", async () => {
|
|
578
|
+
await writeResourceDefs();
|
|
579
|
+
const implFile = join(testDir, "impl.ts");
|
|
580
|
+
await writeFile(
|
|
581
|
+
implFile,
|
|
582
|
+
`
|
|
583
|
+
import { Bucket } from "./resources";
|
|
584
|
+
export const bucket = new Bucket({ name: "impl-bucket" });
|
|
585
|
+
`,
|
|
586
|
+
);
|
|
587
|
+
const barrelFile = join(testDir, "barrel.ts");
|
|
588
|
+
await writeFile(barrelFile, `export { bucket } from "./impl";`);
|
|
589
|
+
|
|
590
|
+
const session = createFoldSession();
|
|
591
|
+
const implResult = await tryFoldFile(implFile, [], session);
|
|
592
|
+
const barrelResult = await tryFoldFile(barrelFile, [], session);
|
|
593
|
+
|
|
594
|
+
expect(implResult.ok).toBe(true);
|
|
595
|
+
expect(barrelResult.ok).toBe(true);
|
|
596
|
+
if (!implResult.ok || !barrelResult.ok) return;
|
|
597
|
+
expect(barrelResult.entities).toHaveLength(1);
|
|
598
|
+
const [name, barrelBucket] = barrelResult.entities[0];
|
|
599
|
+
expect(name).toBe("bucket");
|
|
600
|
+
const [, implBucket] = implResult.entities[0];
|
|
601
|
+
expect(barrelBucket).toBe(implBucket);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("a re-export chain resolves transitively through more than one hop", async () => {
|
|
605
|
+
await writeResourceDefs();
|
|
606
|
+
await writeFile(
|
|
607
|
+
join(testDir, "impl.ts"),
|
|
608
|
+
`
|
|
609
|
+
import { Bucket } from "./resources";
|
|
610
|
+
export const bucket = new Bucket({ name: "impl-bucket" });
|
|
611
|
+
`,
|
|
612
|
+
);
|
|
613
|
+
await writeFile(join(testDir, "mid-barrel.ts"), `export { bucket } from "./impl";`);
|
|
614
|
+
const topBarrel = join(testDir, "top-barrel.ts");
|
|
615
|
+
await writeFile(topBarrel, `export { bucket as sharedBucket } from "./mid-barrel";`);
|
|
616
|
+
|
|
617
|
+
const result = await tryFoldFile(topBarrel);
|
|
618
|
+
|
|
619
|
+
expect(result.ok).toBe(true);
|
|
620
|
+
if (!result.ok) return;
|
|
621
|
+
expect(result.entities).toHaveLength(1);
|
|
622
|
+
const [name, entity] = result.entities[0];
|
|
623
|
+
expect(name).toBe("sharedBucket");
|
|
624
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
625
|
+
expect((entity as unknown as { props: unknown }).props).toEqual({ name: "impl-bucket" });
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Explicit timeouts throughout this describe's cycle tests: a regression
|
|
629
|
+
// that broke cycle detection (rather than just mis-formatting its message)
|
|
630
|
+
// would hang, not fail — a bounded test timeout turns that into a fast,
|
|
631
|
+
// clearly-labeled failure instead of a stuck test run.
|
|
632
|
+
test(
|
|
633
|
+
"an import cycle produces a located FoldError naming the cycle path",
|
|
634
|
+
async () => {
|
|
635
|
+
await writeFile(
|
|
636
|
+
join(testDir, "a.ts"),
|
|
637
|
+
`
|
|
638
|
+
import { B_VALUE } from "./b";
|
|
639
|
+
export const A_VALUE = "a-" + B_VALUE;
|
|
640
|
+
`,
|
|
641
|
+
);
|
|
642
|
+
await writeFile(
|
|
643
|
+
join(testDir, "b.ts"),
|
|
644
|
+
`
|
|
645
|
+
import { A_VALUE } from "./a";
|
|
646
|
+
export const B_VALUE = "b-" + A_VALUE;
|
|
647
|
+
`,
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const result = await tryFoldFile(join(testDir, "a.ts"));
|
|
651
|
+
|
|
652
|
+
expect(result.ok).toBe(false);
|
|
653
|
+
if (result.ok) return;
|
|
654
|
+
expect(result.reason).toContain("import cycle");
|
|
655
|
+
expect(result.reason).toContain("a.ts");
|
|
656
|
+
expect(result.reason).toContain("b.ts");
|
|
657
|
+
// Located — "line:col - message", the same `FoldError` formatting as
|
|
658
|
+
// every other fold rejection, not a bare unpositioned string.
|
|
659
|
+
expect(result.reason).toMatch(/\d+:\d+ - /);
|
|
660
|
+
},
|
|
661
|
+
5000,
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
test(
|
|
665
|
+
"a self-cycle (a file whose own re-export chain reaches back to itself) is also detected",
|
|
666
|
+
async () => {
|
|
667
|
+
await writeFile(join(testDir, "a.ts"), `export { x } from "./b";`);
|
|
668
|
+
const file = join(testDir, "b.ts");
|
|
669
|
+
await writeFile(file, `export { x } from "./a";`);
|
|
670
|
+
|
|
671
|
+
const result = await tryFoldFile(file);
|
|
672
|
+
|
|
673
|
+
expect(result.ok).toBe(false);
|
|
674
|
+
if (result.ok) return;
|
|
675
|
+
expect(result.reason).toContain("import cycle");
|
|
676
|
+
},
|
|
677
|
+
5000,
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
// A 3-file cycle where the loop closes several hops away from where it
|
|
681
|
+
// started (a -> b -> c -> a), not just a direct pair — proves cycle
|
|
682
|
+
// detection walks the whole `stack`, not just the immediate caller.
|
|
683
|
+
test(
|
|
684
|
+
"a longer cycle (three files) is detected, not just a direct pair",
|
|
685
|
+
async () => {
|
|
686
|
+
await writeFile(join(testDir, "a.ts"), `import { c } from "./c";\nexport const a = "a" + c;`);
|
|
687
|
+
await writeFile(join(testDir, "b.ts"), `import { a } from "./a";\nexport const b = "b" + a;`);
|
|
688
|
+
await writeFile(join(testDir, "c.ts"), `import { b } from "./b";\nexport const c = "c" + b;`);
|
|
689
|
+
|
|
690
|
+
const result = await tryFoldFile(join(testDir, "b.ts"));
|
|
691
|
+
|
|
692
|
+
expect(result.ok).toBe(false);
|
|
693
|
+
if (result.ok) return;
|
|
694
|
+
expect(result.reason).toContain("import cycle");
|
|
695
|
+
},
|
|
696
|
+
5000,
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// chant #1039 — the CLI fold path hardcoded `intrinsics` to `[]` at both
|
|
701
|
+
// `fold-import.ts` call sites, so a registered lexicon intrinsic tagged
|
|
702
|
+
// template (e.g. AWS `Sub`\`...\`) never folded in production even though
|
|
703
|
+
// `tryFoldFile` accepted an `intrinsics` parameter. These tests exercise
|
|
704
|
+
// that parameter end-to-end with a REAL tagged-template intrinsic — built on
|
|
705
|
+
// the same `Intrinsic`/`INTRINSIC_MARKER` contract every lexicon's own
|
|
706
|
+
// intrinsic classes implement (see `intrinsicPath` above), not a fabricated
|
|
707
|
+
// shape — rather than the hand-rolled `IntrinsicDef` used by `fold.test.ts`'s
|
|
708
|
+
// narrower unit tests of the reducer alone.
|
|
709
|
+
describe("tryFoldFile — registered intrinsic tagged templates (#1039)", () => {
|
|
710
|
+
let testDir: string;
|
|
711
|
+
|
|
712
|
+
beforeEach(async () => {
|
|
713
|
+
testDir = join(tmpdir(), `chant-fold-import-intrinsic-test-${Date.now()}-${Math.random()}`);
|
|
714
|
+
await mkdir(testDir, { recursive: true });
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
afterEach(async () => {
|
|
718
|
+
await rm(testDir, { recursive: true, force: true });
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const SUB: IntrinsicDef = { name: "Sub", isTag: true, outputKey: "Test::Sub" };
|
|
722
|
+
|
|
723
|
+
/** A tagged-template intrinsic + pseudo-parameter namespace, shaped exactly like a real lexicon's (AWS's `Sub`/`AWS.StackName`): a class implementing `Intrinsic`, and a factory function invoked as `Sub`\`...\``. */
|
|
724
|
+
async function writeIntrinsicDefs(): Promise<void> {
|
|
725
|
+
await writeFile(
|
|
726
|
+
join(testDir, "intrinsics.ts"),
|
|
727
|
+
`
|
|
728
|
+
import { INTRINSIC_MARKER } from ${JSON.stringify(intrinsicPath)};
|
|
729
|
+
|
|
730
|
+
export class SubIntrinsic {
|
|
731
|
+
constructor(strings, values) {
|
|
732
|
+
this[INTRINSIC_MARKER] = true;
|
|
733
|
+
this.strings = strings;
|
|
734
|
+
this.values = values;
|
|
735
|
+
}
|
|
736
|
+
toJSON() {
|
|
737
|
+
let out = "";
|
|
738
|
+
for (let i = 0; i < this.strings.length; i++) {
|
|
739
|
+
out += this.strings[i];
|
|
740
|
+
if (i < this.values.length) out += String(this.values[i]);
|
|
741
|
+
}
|
|
742
|
+
return { "Test::Sub": out };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function Sub(strings, ...values) {
|
|
747
|
+
return new SubIntrinsic(strings, values);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export const NS = { Region: "NS::Region" };
|
|
751
|
+
`,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function writeResourceDefs(): Promise<void> {
|
|
756
|
+
await writeFile(
|
|
757
|
+
join(testDir, "resources.ts"),
|
|
758
|
+
`
|
|
759
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
760
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
761
|
+
`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
test("a registered tag folds end-to-end to the real intrinsic's value — zero module execution", async () => {
|
|
766
|
+
await writeIntrinsicDefs();
|
|
767
|
+
await writeResourceDefs();
|
|
768
|
+
const file = join(testDir, "main.ts");
|
|
769
|
+
await writeFile(
|
|
770
|
+
file,
|
|
771
|
+
`
|
|
772
|
+
import { Bucket } from "./resources";
|
|
773
|
+
import { Sub, NS } from "./intrinsics";
|
|
774
|
+
throw new Error("must never execute — sentinel for #1039 fold verification");
|
|
775
|
+
export const bucket = new Bucket({ name: Sub\`\${NS.Region}-data\` });
|
|
776
|
+
`,
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const result = await tryFoldFile(file, [SUB]);
|
|
780
|
+
|
|
781
|
+
expect(result.ok).toBe(true);
|
|
782
|
+
if (!result.ok) return;
|
|
783
|
+
expect(result.entities).toHaveLength(1);
|
|
784
|
+
const [name, entity] = result.entities[0];
|
|
785
|
+
expect(name).toBe("bucket");
|
|
786
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
787
|
+
// The revived value must be a genuine, live `SubIntrinsic` instance —
|
|
788
|
+
// constructed by actually invoking the real `Sub` tag function through
|
|
789
|
+
// this file's own imports, with "NS::Region" resolved via its `NS`
|
|
790
|
+
// import — not the raw `{ __intrinsic, strings, values }` / `{ __symbol
|
|
791
|
+
// }` envelope fold() produces internally (that envelope is meant to be
|
|
792
|
+
// replayed, not serialized — see fold-import.ts's "Intrinsic revival"
|
|
793
|
+
// section). A serializer calls `.toJSON()` on it exactly like it would
|
|
794
|
+
// for any other lexicon intrinsic (e.g. AWS's real `Sub`).
|
|
795
|
+
const bucketName = (entity as unknown as { props: { name: unknown } }).props.name;
|
|
796
|
+
expect(bucketName).toBeInstanceOf(Object);
|
|
797
|
+
expect((bucketName as { toJSON(): unknown }).toJSON()).toEqual({ "Test::Sub": "NS::Region-data" });
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test("an unregistered tag still falls back to run when no intrinsics are passed", async () => {
|
|
801
|
+
await writeIntrinsicDefs();
|
|
802
|
+
await writeResourceDefs();
|
|
803
|
+
const file = join(testDir, "main.ts");
|
|
804
|
+
await writeFile(
|
|
805
|
+
file,
|
|
806
|
+
`
|
|
807
|
+
import { Bucket } from "./resources";
|
|
808
|
+
import { Sub, NS } from "./intrinsics";
|
|
809
|
+
export const bucket = new Bucket({ name: Sub\`\${NS.Region}-data\` });
|
|
810
|
+
`,
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
// No second argument — reproduces the exact pre-#1039 production bug
|
|
814
|
+
// (the CLI fold path hardcoded this to `[]`).
|
|
815
|
+
const result = await tryFoldFile(file);
|
|
816
|
+
|
|
817
|
+
expect(result.ok).toBe(false);
|
|
818
|
+
if (result.ok) return;
|
|
819
|
+
expect(result.reason).toContain("unregistered tagged template intrinsic: Sub");
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("a same-file resource reference inside a folded intrinsic's interpolation falls back to run rather than folding to the wrong value", async () => {
|
|
823
|
+
await writeIntrinsicDefs();
|
|
824
|
+
await writeResourceDefs();
|
|
825
|
+
const file = join(testDir, "main.ts");
|
|
826
|
+
await writeFile(
|
|
827
|
+
file,
|
|
828
|
+
`
|
|
829
|
+
import { Bucket } from "./resources";
|
|
830
|
+
import { Sub } from "./intrinsics";
|
|
831
|
+
export const a = new Bucket({ name: "a" });
|
|
832
|
+
export const b = new Bucket({ name: Sub\`\${a.arn}-clone\` });
|
|
833
|
+
`,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
const result = await tryFoldFile(file, [SUB]);
|
|
837
|
+
|
|
838
|
+
// Reviving would need a genuine live `AttrRef` wired to the sibling
|
|
839
|
+
// entity (a WeakRef into an object this file's fold hasn't necessarily
|
|
840
|
+
// constructed yet) — out of scope for #1039, same call as the existing
|
|
841
|
+
// "nested `new Type(...)` as a value" rejection. Falling back to run is
|
|
842
|
+
// always safe; silently folding to the raw envelope would not be.
|
|
843
|
+
expect(result.ok).toBe(false);
|
|
844
|
+
if (result.ok) return;
|
|
845
|
+
expect(result.reason).toContain("not foldable");
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
describe("tryFoldFile — build-time parameters (chant #1064)", () => {
|
|
850
|
+
let testDir: string;
|
|
851
|
+
|
|
852
|
+
beforeEach(async () => {
|
|
853
|
+
testDir = join(tmpdir(), `chant-fold-import-buildparams-test-${Date.now()}-${Math.random()}`);
|
|
854
|
+
await mkdir(testDir, { recursive: true });
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
afterEach(async () => {
|
|
858
|
+
await rm(testDir, { recursive: true, force: true });
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
async function writeResourceDefs(): Promise<void> {
|
|
862
|
+
await writeFile(
|
|
863
|
+
join(testDir, "resources.ts"),
|
|
864
|
+
`
|
|
865
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
866
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
867
|
+
`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
test("a direct params.<name> declarator folds to a LITERAL, not a symbolic node", async () => {
|
|
872
|
+
const file = join(testDir, "main.ts");
|
|
873
|
+
await writeFile(
|
|
874
|
+
file,
|
|
875
|
+
`
|
|
876
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
877
|
+
export const tier = params.tier;
|
|
878
|
+
`,
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
const session = createFoldSession([], { tier: "production" });
|
|
882
|
+
const result = await tryFoldFile(file, [], session);
|
|
883
|
+
|
|
884
|
+
expect(result.ok).toBe(true);
|
|
885
|
+
if (!result.ok) return;
|
|
886
|
+
expect(result.exportedValues.get("tier")).toBe("production");
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("params.<name> nested inside a resource's props folds through the general reducer", async () => {
|
|
890
|
+
await writeResourceDefs();
|
|
891
|
+
const file = join(testDir, "main.ts");
|
|
892
|
+
await writeFile(
|
|
893
|
+
file,
|
|
894
|
+
`
|
|
895
|
+
import { Bucket } from "./resources";
|
|
896
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
897
|
+
export const bucket = new Bucket({ name: params.env });
|
|
898
|
+
`,
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
const session = createFoldSession([], { env: "staging" });
|
|
902
|
+
const result = await tryFoldFile(file, [], session);
|
|
903
|
+
|
|
904
|
+
expect(result.ok).toBe(true);
|
|
905
|
+
if (!result.ok) return;
|
|
906
|
+
expect(result.entities).toHaveLength(1);
|
|
907
|
+
const [, entity] = result.entities[0];
|
|
908
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
909
|
+
expect((entity as unknown as { props: { name: unknown } }).props.name).toBe("staging");
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test("a nullish-coalesced default still folds to a literal (loomster's `params.x ?? \"default\"` pattern)", async () => {
|
|
913
|
+
const file = join(testDir, "main.ts");
|
|
914
|
+
await writeFile(
|
|
915
|
+
file,
|
|
916
|
+
`
|
|
917
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
918
|
+
export const project = params.project ?? "loom";
|
|
919
|
+
`,
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
// No "project" key supplied — falls through to the literal default.
|
|
923
|
+
const session = createFoldSession([], {});
|
|
924
|
+
const result = await tryFoldFile(file, [], session);
|
|
925
|
+
|
|
926
|
+
expect(result.ok).toBe(true);
|
|
927
|
+
if (!result.ok) return;
|
|
928
|
+
expect(result.exportedValues.get("project")).toBe("loom");
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("an unrelated import also named `params` (not resolving to ../params.ts) is unaffected", async () => {
|
|
932
|
+
await writeFile(
|
|
933
|
+
join(testDir, "local-config.ts"),
|
|
934
|
+
`export const params = { tier: "should-not-be-used" };`,
|
|
935
|
+
);
|
|
936
|
+
const file = join(testDir, "main.ts");
|
|
937
|
+
await writeFile(
|
|
938
|
+
file,
|
|
939
|
+
`
|
|
940
|
+
import { params } from "./local-config";
|
|
941
|
+
export const tier = params.tier;
|
|
942
|
+
`,
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const session = createFoldSession([], { tier: "production" });
|
|
946
|
+
const result = await tryFoldFile(file, [], session);
|
|
947
|
+
|
|
948
|
+
expect(result.ok).toBe(true);
|
|
949
|
+
if (!result.ok) return;
|
|
950
|
+
// Resolved via the ORDINARY cross-file project resolution, not the
|
|
951
|
+
// build-time-parameters substitution — proves the two never collide.
|
|
952
|
+
expect(result.exportedValues.get("tier")).toBe("should-not-be-used");
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("a `params`-named import from an unrelated BARE specifier is left alone (text-match only, never resolved)", async () => {
|
|
956
|
+
// Regression guard: buildExternals must recognize the build-time-params
|
|
957
|
+
// bare specifier by an exact TEXT match, never by resolving an arbitrary
|
|
958
|
+
// bare specifier just because the imported binding happens to be named
|
|
959
|
+
// "params" — resolving every such specifier would reintroduce the exact
|
|
960
|
+
// cold bare-specifier-resolution cost chant#1020 already fixed (up to
|
|
961
|
+
// ~361s for a genuinely new package's first resolution in a process).
|
|
962
|
+
// "left-pad" is never installed in this repo, so if this DID try to
|
|
963
|
+
// resolve it, it would throw (caught) rather than hang — this test
|
|
964
|
+
// mainly documents the invariant; the perf regression itself was only
|
|
965
|
+
// observable via a real corpus/CI run, not a unit test.
|
|
966
|
+
const file = join(testDir, "main.ts");
|
|
967
|
+
await writeFile(
|
|
968
|
+
file,
|
|
969
|
+
`
|
|
970
|
+
import { params } from "left-pad";
|
|
971
|
+
export const tier = params.tier;
|
|
972
|
+
`,
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
const session = createFoldSession([], { tier: "production" });
|
|
976
|
+
const result = await tryFoldFile(file, [], session);
|
|
977
|
+
|
|
978
|
+
// Not substituted from build-time parameters (would be "production" if
|
|
979
|
+
// it were) — falls back to run via the ordinary unresolved-bare-specifier
|
|
980
|
+
// path, exactly as any other unimportable bare specifier would.
|
|
981
|
+
expect(result.ok).toBe(false);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
test("without a FoldSession buildParams, params.<name> is an unresolved identifier (matches an ordinary bare import)", async () => {
|
|
985
|
+
const file = join(testDir, "main.ts");
|
|
986
|
+
await writeFile(
|
|
987
|
+
file,
|
|
988
|
+
`
|
|
989
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
990
|
+
export const tier = params.tier;
|
|
991
|
+
`,
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
// No session passed at all — tryFoldFile creates a private one with no buildParams.
|
|
995
|
+
const result = await tryFoldFile(file);
|
|
996
|
+
|
|
997
|
+
expect(result.ok).toBe(false);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("real end-to-end parity: the fold path substitutes without ever importing ../params.ts, while a real run-fallback import sees the same values via setBuildParams", async () => {
|
|
1001
|
+
// Simulates what discover() does before either fold or run touches a file.
|
|
1002
|
+
const { setBuildParams } = await import("../params");
|
|
1003
|
+
setBuildParams({ tier: "production-ha" });
|
|
1004
|
+
|
|
1005
|
+
const file = join(testDir, "main.ts");
|
|
1006
|
+
await writeFile(
|
|
1007
|
+
file,
|
|
1008
|
+
`
|
|
1009
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
1010
|
+
export const tier = params.tier;
|
|
1011
|
+
`,
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
const session = createFoldSession([], { tier: "production-ha" });
|
|
1015
|
+
const folded = await tryFoldFile(file, [], session);
|
|
1016
|
+
expect(folded.ok).toBe(true);
|
|
1017
|
+
if (folded.ok) expect(folded.exportedValues.get("tier")).toBe("production-ha");
|
|
1018
|
+
|
|
1019
|
+
// A real import of the shared module (what a run-fallback file's own
|
|
1020
|
+
// `import()` would do) observes the identical value — same object,
|
|
1021
|
+
// mutated in place, not re-bound.
|
|
1022
|
+
expect(sharedParams.tier).toBe("production-ha");
|
|
1023
|
+
|
|
1024
|
+
setBuildParams({});
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* chant #1082 — registered authoring helpers, and constructor argument
|
|
1030
|
+
* positions.
|
|
1031
|
+
*
|
|
1032
|
+
* Fixtures import chant-core's REAL helpers by absolute path (the convention
|
|
1033
|
+
* every other suite in this file uses), which is also the second arm of the
|
|
1034
|
+
* provenance check: a specifier that resolves inside chant-core's own tree
|
|
1035
|
+
* counts as chant's own, exactly like the `@intentius/chant*` package
|
|
1036
|
+
* specifier a real project writes.
|
|
1037
|
+
*/
|
|
1038
|
+
describe("tryFoldFile — registered authoring helpers (#1082)", () => {
|
|
1039
|
+
let testDir: string;
|
|
1040
|
+
|
|
1041
|
+
beforeEach(async () => {
|
|
1042
|
+
testDir = join(tmpdir(), `chant-fold-import-helpers-test-${Date.now()}-${Math.random()}`);
|
|
1043
|
+
await mkdir(testDir, { recursive: true });
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
afterEach(async () => {
|
|
1047
|
+
await rm(testDir, { recursive: true, force: true });
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
/** Absolute path to the real component-authoring helpers (`phase`, `gate`, `stackOutput`). */
|
|
1051
|
+
const componentPath = resolve(thisDir, "../components/component");
|
|
1052
|
+
/** Absolute path to the real `output()` / `LexiconOutput`. */
|
|
1053
|
+
const lexiconOutputPath = resolve(thisDir, "../lexicon-output");
|
|
1054
|
+
|
|
1055
|
+
test("a component authored with phase()/gate()/stackOutput() folds to the real plain data — zero module execution", async () => {
|
|
1056
|
+
const file = join(testDir, "web.component.ts");
|
|
1057
|
+
await writeFile(
|
|
1058
|
+
file,
|
|
1059
|
+
`
|
|
1060
|
+
import { phase, gate, stackOutput } from ${JSON.stringify(componentPath)};
|
|
1061
|
+
throw new Error("must never execute — sentinel for #1082 fold verification");
|
|
1062
|
+
export const web = {
|
|
1063
|
+
name: "web",
|
|
1064
|
+
dependsOn: ["shared-foundation"],
|
|
1065
|
+
deploy: [
|
|
1066
|
+
phase("Apply", [
|
|
1067
|
+
{ kind: "cfn-deploy", stack: "web", inputs: { pVpcId: stackOutput("shared-foundation", "oVpcId") } },
|
|
1068
|
+
gate("approve", { timeout: "24h" }),
|
|
1069
|
+
], { parallel: true }),
|
|
1070
|
+
],
|
|
1071
|
+
};
|
|
1072
|
+
`,
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
const result = await tryFoldFile(file);
|
|
1076
|
+
|
|
1077
|
+
expect(result.ok).toBe(true);
|
|
1078
|
+
if (!result.ok) return;
|
|
1079
|
+
// The revived value is what the real helpers return, not an envelope:
|
|
1080
|
+
// `phase()`'s `{ phase, steps, parallel }`, `gate()`'s `{ kind: "gate", … }`,
|
|
1081
|
+
// `stackOutput()`'s `{ stackOutput: { stack, name } }`.
|
|
1082
|
+
expect(result.exportedValues.get("web")).toEqual({
|
|
1083
|
+
name: "web",
|
|
1084
|
+
dependsOn: ["shared-foundation"],
|
|
1085
|
+
deploy: [
|
|
1086
|
+
{
|
|
1087
|
+
phase: "Apply",
|
|
1088
|
+
parallel: true,
|
|
1089
|
+
steps: [
|
|
1090
|
+
{
|
|
1091
|
+
kind: "cfn-deploy",
|
|
1092
|
+
stack: "web",
|
|
1093
|
+
inputs: { pVpcId: { stackOutput: { stack: "shared-foundation", name: "oVpcId" } } },
|
|
1094
|
+
},
|
|
1095
|
+
{ kind: "gate", signalName: "approve", timeout: "24h" },
|
|
1096
|
+
],
|
|
1097
|
+
},
|
|
1098
|
+
],
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
test("a nested (fan-out) phase folds — helper envelopes revive bottom-up", async () => {
|
|
1103
|
+
const file = join(testDir, "fanout.component.ts");
|
|
1104
|
+
await writeFile(
|
|
1105
|
+
file,
|
|
1106
|
+
`
|
|
1107
|
+
import { phase } from ${JSON.stringify(componentPath)};
|
|
1108
|
+
export const fanout = { deploy: [phase("Outer", [phase("Inner", [{ kind: "noop" }])])] };
|
|
1109
|
+
`,
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
const result = await tryFoldFile(file);
|
|
1113
|
+
|
|
1114
|
+
expect(result.ok).toBe(true);
|
|
1115
|
+
if (!result.ok) return;
|
|
1116
|
+
expect(result.exportedValues.get("fanout")).toEqual({
|
|
1117
|
+
deploy: [{ phase: "Outer", steps: [{ phase: "Inner", steps: [{ kind: "noop" }] }] }],
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
test("a registered NAME imported from the project's own code does NOT fold — the allowlist is chant's, not the name's", async () => {
|
|
1122
|
+
await writeFile(
|
|
1123
|
+
join(testDir, "helpers.ts"),
|
|
1124
|
+
`export function phase(name, steps) { return { phase: name, steps, mine: true }; }`,
|
|
1125
|
+
);
|
|
1126
|
+
const file = join(testDir, "web.component.ts");
|
|
1127
|
+
await writeFile(
|
|
1128
|
+
file,
|
|
1129
|
+
`
|
|
1130
|
+
import { phase } from "./helpers";
|
|
1131
|
+
export const web = { deploy: [phase("Apply", [])] };
|
|
1132
|
+
`,
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
const result = await tryFoldFile(file);
|
|
1136
|
+
|
|
1137
|
+
expect(result.ok).toBe(false);
|
|
1138
|
+
if (result.ok) return;
|
|
1139
|
+
expect(result.reason).toContain("./helpers");
|
|
1140
|
+
expect(result.reason).toContain("not chant's own");
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
test("a registered name declared in the file itself does NOT fold — it is not an import at all", async () => {
|
|
1144
|
+
const file = join(testDir, "local.component.ts");
|
|
1145
|
+
await writeFile(
|
|
1146
|
+
file,
|
|
1147
|
+
`
|
|
1148
|
+
function phase(name, steps) { return { phase: name, steps }; }
|
|
1149
|
+
export const web = { deploy: [phase("Apply", [])] };
|
|
1150
|
+
`,
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
const result = await tryFoldFile(file);
|
|
1154
|
+
|
|
1155
|
+
expect(result.ok).toBe(false);
|
|
1156
|
+
if (result.ok) return;
|
|
1157
|
+
expect(result.reason).toContain("is not a resolvable import");
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
test("an UNREGISTERED chant import called as a value still falls back — registration is per-helper, not per-module", async () => {
|
|
1161
|
+
const file = join(testDir, "unregistered.component.ts");
|
|
1162
|
+
await writeFile(
|
|
1163
|
+
file,
|
|
1164
|
+
`
|
|
1165
|
+
import { inferArchetype } from ${JSON.stringify(componentPath)};
|
|
1166
|
+
export const web = { archetype: inferArchetype({ deploy: [] }) };
|
|
1167
|
+
`,
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
const result = await tryFoldFile(file);
|
|
1171
|
+
|
|
1172
|
+
expect(result.ok).toBe(false);
|
|
1173
|
+
if (result.ok) return;
|
|
1174
|
+
expect(result.reason).toContain("inferArchetype(...)");
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test("output() folds against a REAL cross-file AttrRef, producing a genuine LexiconOutput", async () => {
|
|
1178
|
+
await writeFile(
|
|
1179
|
+
join(testDir, "defs.ts"),
|
|
1180
|
+
`
|
|
1181
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1182
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
1183
|
+
`,
|
|
1184
|
+
);
|
|
1185
|
+
await writeFile(
|
|
1186
|
+
join(testDir, "resources.ts"),
|
|
1187
|
+
`
|
|
1188
|
+
import { Bucket } from "./defs";
|
|
1189
|
+
export const bucket = new Bucket({ name: "my-bucket" });
|
|
1190
|
+
`,
|
|
1191
|
+
);
|
|
1192
|
+
const file = join(testDir, "outputs.ts");
|
|
1193
|
+
await writeFile(
|
|
1194
|
+
file,
|
|
1195
|
+
`
|
|
1196
|
+
import { output } from ${JSON.stringify(lexiconOutputPath)};
|
|
1197
|
+
import { bucket } from "./resources";
|
|
1198
|
+
export const oArn = bucket ? output(bucket.arn, "oArn") : undefined;
|
|
1199
|
+
`,
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
const result = await tryFoldFile(file);
|
|
1203
|
+
|
|
1204
|
+
expect(result.ok).toBe(true);
|
|
1205
|
+
if (!result.ok) return;
|
|
1206
|
+
const oArn = result.exportedValues.get("oArn") as { outputName: string; sourceLexicon: string };
|
|
1207
|
+
// A genuine LexiconOutput built from the real AttrRef — it read the ref's
|
|
1208
|
+
// parent through its WeakRef to learn the lexicon, which is only possible
|
|
1209
|
+
// with a live reference, never with a `{ __attrRef }` envelope.
|
|
1210
|
+
expect(oArn.outputName).toBe("oArn");
|
|
1211
|
+
expect(oArn.sourceLexicon).toBe("aws");
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
test("output() over a SAME-FILE resource reference falls back rather than wrapping a symbolic envelope", async () => {
|
|
1215
|
+
await writeFile(
|
|
1216
|
+
join(testDir, "defs.ts"),
|
|
1217
|
+
`
|
|
1218
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1219
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
1220
|
+
`,
|
|
1221
|
+
);
|
|
1222
|
+
const file = join(testDir, "same-file-outputs.ts");
|
|
1223
|
+
await writeFile(
|
|
1224
|
+
file,
|
|
1225
|
+
`
|
|
1226
|
+
import { Bucket } from "./defs";
|
|
1227
|
+
import { output } from ${JSON.stringify(lexiconOutputPath)};
|
|
1228
|
+
const enabled = true;
|
|
1229
|
+
export const bucket = new Bucket({ name: "my-bucket" });
|
|
1230
|
+
export const oArn = enabled ? output(bucket.arn, "oArn") : undefined;
|
|
1231
|
+
`,
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
const result = await tryFoldFile(file);
|
|
1235
|
+
|
|
1236
|
+
// `bucket.arn` folds to a `{ __attrRef }` envelope, and `LexiconOutput`'s
|
|
1237
|
+
// constructor needs a real `AttrRef` (it derefs the parent to learn the
|
|
1238
|
+
// lexicon). Constructing it from the envelope would silently produce a
|
|
1239
|
+
// wrong output, so the whole file falls back to run instead.
|
|
1240
|
+
expect(result.ok).toBe(false);
|
|
1241
|
+
if (result.ok) return;
|
|
1242
|
+
expect(result.reason).toContain("same-file resource reference");
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
describe("tryFoldFile — constructor argument positions (#1082)", () => {
|
|
1247
|
+
let testDir: string;
|
|
1248
|
+
|
|
1249
|
+
beforeEach(async () => {
|
|
1250
|
+
testDir = join(tmpdir(), `chant-fold-import-ctorargs-test-${Date.now()}-${Math.random()}`);
|
|
1251
|
+
await mkdir(testDir, { recursive: true });
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
afterEach(async () => {
|
|
1255
|
+
await rm(testDir, { recursive: true, force: true });
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
/** A `(type, props)` constructor, structurally identical to the real AWS deploy-time `Parameter` (lexicons/aws/src/parameter.ts). */
|
|
1259
|
+
async function writeParameterDef(): Promise<void> {
|
|
1260
|
+
await writeFile(
|
|
1261
|
+
join(testDir, "parameter.ts"),
|
|
1262
|
+
`
|
|
1263
|
+
import { DECLARABLE_MARKER } from ${JSON.stringify(resolve(thisDir, "../declarable"))};
|
|
1264
|
+
|
|
1265
|
+
export class Parameter {
|
|
1266
|
+
constructor(type, options) {
|
|
1267
|
+
this[DECLARABLE_MARKER] = true;
|
|
1268
|
+
this.lexicon = "aws";
|
|
1269
|
+
this.entityType = "AWS::CloudFormation::Parameter";
|
|
1270
|
+
this.parameterType = type;
|
|
1271
|
+
this.description = options?.description;
|
|
1272
|
+
this.defaultValue = options?.defaultValue;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
`,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
test("`new Parameter(\"String\", {...})` folds — the props object need not be the first argument", async () => {
|
|
1280
|
+
await writeParameterDef();
|
|
1281
|
+
const file = join(testDir, "params.ts");
|
|
1282
|
+
await writeFile(
|
|
1283
|
+
file,
|
|
1284
|
+
`
|
|
1285
|
+
import { Parameter } from "./parameter";
|
|
1286
|
+
throw new Error("must never execute — sentinel for #1082 fold verification");
|
|
1287
|
+
export const pVpcId = new Parameter("AWS::EC2::VPC::Id", { description: "vpc id" });
|
|
1288
|
+
`,
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
const result = await tryFoldFile(file);
|
|
1292
|
+
|
|
1293
|
+
expect(result.ok).toBe(true);
|
|
1294
|
+
if (!result.ok) return;
|
|
1295
|
+
expect(result.entities).toHaveLength(1);
|
|
1296
|
+
const [name, entity] = result.entities[0];
|
|
1297
|
+
expect(name).toBe("pVpcId");
|
|
1298
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
1299
|
+
// Both arguments reached the real constructor, in the right positions.
|
|
1300
|
+
expect((entity as unknown as { parameterType: string }).parameterType).toBe("AWS::EC2::VPC::Id");
|
|
1301
|
+
expect((entity as unknown as { description: string }).description).toBe("vpc id");
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
test("a constructor called with only a non-object argument folds too — no props object is invented", async () => {
|
|
1305
|
+
await writeParameterDef();
|
|
1306
|
+
const file = join(testDir, "params.ts");
|
|
1307
|
+
await writeFile(
|
|
1308
|
+
file,
|
|
1309
|
+
`
|
|
1310
|
+
import { Parameter } from "./parameter";
|
|
1311
|
+
export const pName = new Parameter("String");
|
|
1312
|
+
`,
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
const result = await tryFoldFile(file);
|
|
1316
|
+
|
|
1317
|
+
expect(result.ok).toBe(true);
|
|
1318
|
+
if (!result.ok) return;
|
|
1319
|
+
const [, entity] = result.entities[0];
|
|
1320
|
+
expect((entity as unknown as { parameterType: string }).parameterType).toBe("String");
|
|
1321
|
+
expect((entity as unknown as { description?: string }).description).toBeUndefined();
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
test("a non-foldable argument in any position still falls the file back to run", async () => {
|
|
1325
|
+
await writeParameterDef();
|
|
1326
|
+
const file = join(testDir, "params.ts");
|
|
1327
|
+
await writeFile(
|
|
1328
|
+
file,
|
|
1329
|
+
`
|
|
1330
|
+
import { Parameter } from "./parameter";
|
|
1331
|
+
export const pName = new Parameter(computeType(), { description: "x" });
|
|
1332
|
+
`,
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
const result = await tryFoldFile(file);
|
|
1336
|
+
|
|
1337
|
+
expect(result.ok).toBe(false);
|
|
1338
|
+
if (result.ok) return;
|
|
1339
|
+
expect(result.reason).toContain("computeType(...)");
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* chant #1044 — registered lexicon intrinsics in PLAIN-CALL form, end to end.
|
|
1345
|
+
*
|
|
1346
|
+
* The unit tests in ../fold/fold.test.ts cover the reducer's half (a call
|
|
1347
|
+
* reduces to a `{__intrinsic, args}` envelope, executing nothing). These
|
|
1348
|
+
* cover the other half: the envelope is revived by resolving the name
|
|
1349
|
+
* through THIS FILE'S OWN imports and invoking the real function, so what
|
|
1350
|
+
* lands in a resource's props is a genuine live intrinsic instance — the
|
|
1351
|
+
* same object the run path would have built.
|
|
1352
|
+
*/
|
|
1353
|
+
describe("tryFoldFile — registered call-form intrinsics (#1044)", () => {
|
|
1354
|
+
let testDir: string;
|
|
1355
|
+
|
|
1356
|
+
beforeEach(async () => {
|
|
1357
|
+
testDir = join(tmpdir(), `chant-fold-import-callintrinsic-test-${Date.now()}-${Math.random()}`);
|
|
1358
|
+
await mkdir(testDir, { recursive: true });
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
afterEach(async () => {
|
|
1362
|
+
await rm(testDir, { recursive: true, force: true });
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
const REF: IntrinsicDef = { name: "Ref", isTag: false, foldsAsCall: true, outputKey: "Test::Ref" };
|
|
1366
|
+
const NOT_OPTED_IN: IntrinsicDef = { name: "Ref", isTag: false };
|
|
1367
|
+
|
|
1368
|
+
/** A plain-call intrinsic shaped exactly like aws's real `Ref`: a factory returning a class that implements the `Intrinsic` contract and resolves its target at `toJSON()` time. */
|
|
1369
|
+
async function writeCallIntrinsicDefs(): Promise<void> {
|
|
1370
|
+
await writeFile(
|
|
1371
|
+
join(testDir, "intrinsics.ts"),
|
|
1372
|
+
`
|
|
1373
|
+
import { INTRINSIC_MARKER } from ${JSON.stringify(intrinsicPath)};
|
|
1374
|
+
|
|
1375
|
+
export class RefIntrinsic {
|
|
1376
|
+
constructor(target) {
|
|
1377
|
+
this[INTRINSIC_MARKER] = true;
|
|
1378
|
+
this.target = target;
|
|
1379
|
+
}
|
|
1380
|
+
toJSON() {
|
|
1381
|
+
return { "Test::Ref": typeof this.target === "string" ? this.target : this.target.logicalHint };
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
export function Ref(target) {
|
|
1386
|
+
return new RefIntrinsic(target);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
export const NS = { Region: "NS::Region" };
|
|
1390
|
+
`,
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
test("an opted-in call folds end-to-end to the real intrinsic instance — zero module execution", async () => {
|
|
1395
|
+
await writeCallIntrinsicDefs();
|
|
1396
|
+
await writeFile(
|
|
1397
|
+
join(testDir, "resources.ts"),
|
|
1398
|
+
`
|
|
1399
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1400
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
1401
|
+
`,
|
|
1402
|
+
);
|
|
1403
|
+
const file = join(testDir, "main.ts");
|
|
1404
|
+
await writeFile(
|
|
1405
|
+
file,
|
|
1406
|
+
`
|
|
1407
|
+
import { Bucket } from "./resources";
|
|
1408
|
+
import { Ref } from "./intrinsics";
|
|
1409
|
+
throw new Error("must never execute — sentinel for #1044 fold verification");
|
|
1410
|
+
export const bucket = new Bucket({ name: Ref("environment") });
|
|
1411
|
+
`,
|
|
1412
|
+
);
|
|
1413
|
+
|
|
1414
|
+
const result = await tryFoldFile(file, [REF]);
|
|
1415
|
+
|
|
1416
|
+
expect(result.ok).toBe(true);
|
|
1417
|
+
if (!result.ok) return;
|
|
1418
|
+
const [, entity] = result.entities[0];
|
|
1419
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
1420
|
+
const name = (entity as unknown as { props: { name: unknown } }).props.name;
|
|
1421
|
+
// A genuine live instance built by the file's own `Ref`, not the
|
|
1422
|
+
// `{ __intrinsic, args }` envelope the reducer produced internally.
|
|
1423
|
+
expect((name as { toJSON(): unknown }).toJSON()).toEqual({ "Test::Ref": "environment" });
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test("a call-form intrinsic folds inside a registered tag's interpolation, and a symbolic argument resolves through the file's own import", async () => {
|
|
1427
|
+
await writeFile(
|
|
1428
|
+
join(testDir, "intrinsics.ts"),
|
|
1429
|
+
`
|
|
1430
|
+
import { INTRINSIC_MARKER } from ${JSON.stringify(intrinsicPath)};
|
|
1431
|
+
export class RefIntrinsic {
|
|
1432
|
+
constructor(target) { this[INTRINSIC_MARKER] = true; this.target = target; }
|
|
1433
|
+
toJSON() { return { "Test::Ref": this.target }; }
|
|
1434
|
+
}
|
|
1435
|
+
export function Ref(target) { return new RefIntrinsic(target); }
|
|
1436
|
+
export class SubIntrinsic {
|
|
1437
|
+
constructor(strings, values) { this[INTRINSIC_MARKER] = true; this.strings = strings; this.values = values; }
|
|
1438
|
+
toJSON() {
|
|
1439
|
+
let out = "";
|
|
1440
|
+
for (let i = 0; i < this.strings.length; i++) {
|
|
1441
|
+
out += this.strings[i];
|
|
1442
|
+
if (i < this.values.length) out += JSON.stringify(this.values[i].toJSON ? this.values[i].toJSON() : this.values[i]);
|
|
1443
|
+
}
|
|
1444
|
+
return { "Test::Sub": out };
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
export function Sub(strings, ...values) { return new SubIntrinsic(strings, values); }
|
|
1448
|
+
export const NS = { Region: "NS::Region" };
|
|
1449
|
+
`,
|
|
1450
|
+
);
|
|
1451
|
+
await writeFile(
|
|
1452
|
+
join(testDir, "resources.ts"),
|
|
1453
|
+
`
|
|
1454
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1455
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
1456
|
+
`,
|
|
1457
|
+
);
|
|
1458
|
+
const file = join(testDir, "main.ts");
|
|
1459
|
+
await writeFile(
|
|
1460
|
+
file,
|
|
1461
|
+
`
|
|
1462
|
+
import { Bucket } from "./resources";
|
|
1463
|
+
import { Sub, Ref, NS } from "./intrinsics";
|
|
1464
|
+
export const bucket = new Bucket({ name: Sub\`\${Ref(NS.Region)}-fn\` });
|
|
1465
|
+
`,
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
const result = await tryFoldFile(file, [REF, { name: "Sub", isTag: true }]);
|
|
1469
|
+
|
|
1470
|
+
expect(result.ok).toBe(true);
|
|
1471
|
+
if (!result.ok) return;
|
|
1472
|
+
const [, entity] = result.entities[0];
|
|
1473
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
1474
|
+
const name = (entity as unknown as { props: { name: unknown } }).props.name;
|
|
1475
|
+
expect((name as { toJSON(): unknown }).toJSON()).toEqual({
|
|
1476
|
+
"Test::Sub": `{"Test::Ref":"NS::Region"}-fn`,
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
test("a cross-file resource passed to an intrinsic call arrives as the SHARED live instance, not a re-imported copy", async () => {
|
|
1481
|
+
await writeCallIntrinsicDefs();
|
|
1482
|
+
await writeFile(
|
|
1483
|
+
join(testDir, "resources.ts"),
|
|
1484
|
+
`
|
|
1485
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1486
|
+
export const Param = createResource("Test::Param", "aws", {});
|
|
1487
|
+
`,
|
|
1488
|
+
);
|
|
1489
|
+
await writeFile(
|
|
1490
|
+
join(testDir, "params.ts"),
|
|
1491
|
+
`
|
|
1492
|
+
import { Param } from "./resources";
|
|
1493
|
+
export const environment = new Param({ logicalHint: "EnvParam" });
|
|
1494
|
+
`,
|
|
1495
|
+
);
|
|
1496
|
+
const file = join(testDir, "main.ts");
|
|
1497
|
+
await writeFile(
|
|
1498
|
+
file,
|
|
1499
|
+
`
|
|
1500
|
+
import { Param } from "./resources";
|
|
1501
|
+
import { environment } from "./params";
|
|
1502
|
+
import { Ref } from "./intrinsics";
|
|
1503
|
+
export const other = new Param({ name: Ref(environment) });
|
|
1504
|
+
`,
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
const session = createFoldSession([REF]);
|
|
1508
|
+
const paramsResult = await tryFoldFile(join(testDir, "params.ts"), [REF], session);
|
|
1509
|
+
const result = await tryFoldFile(file, [REF], session);
|
|
1510
|
+
|
|
1511
|
+
expect(result.ok).toBe(true);
|
|
1512
|
+
if (!result.ok || !paramsResult.ok) return;
|
|
1513
|
+
const [, entity] = result.entities[0];
|
|
1514
|
+
if (!isDeclarable(entity)) throw new Error("expected a Declarable");
|
|
1515
|
+
const ref = (entity as unknown as { props: { name: { target: unknown } } }).props.name;
|
|
1516
|
+
// Identity, not equality: the SAME object params.ts's own fold produced
|
|
1517
|
+
// and discovery will collect. A second instance would serialize with no
|
|
1518
|
+
// logical name at all (see planFoldTaint's doc).
|
|
1519
|
+
expect(ref.target).toBe(paramsResult.exportedValues.get("environment"));
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
test("a REGISTERED intrinsic with no opt-in still falls the file back to run", async () => {
|
|
1523
|
+
await writeCallIntrinsicDefs();
|
|
1524
|
+
await writeFile(
|
|
1525
|
+
join(testDir, "resources.ts"),
|
|
1526
|
+
`
|
|
1527
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1528
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
1529
|
+
`,
|
|
1530
|
+
);
|
|
1531
|
+
const file = join(testDir, "main.ts");
|
|
1532
|
+
await writeFile(
|
|
1533
|
+
file,
|
|
1534
|
+
`
|
|
1535
|
+
import { Bucket } from "./resources";
|
|
1536
|
+
import { Ref } from "./intrinsics";
|
|
1537
|
+
export const bucket = new Bucket({ name: Ref("environment") });
|
|
1538
|
+
`,
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
const result = await tryFoldFile(file, [NOT_OPTED_IN]);
|
|
1542
|
+
|
|
1543
|
+
expect(result.ok).toBe(false);
|
|
1544
|
+
if (result.ok) return;
|
|
1545
|
+
expect(result.reason).toContain("function call as a value is not foldable: Ref(...)");
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
test("an opted-in NAME that this file never imported falls back — the registry is not permission to invoke a name", async () => {
|
|
1549
|
+
await writeFile(
|
|
1550
|
+
join(testDir, "resources.ts"),
|
|
1551
|
+
`
|
|
1552
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1553
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
1554
|
+
`,
|
|
1555
|
+
);
|
|
1556
|
+
const file = join(testDir, "main.ts");
|
|
1557
|
+
await writeFile(
|
|
1558
|
+
file,
|
|
1559
|
+
`
|
|
1560
|
+
import { Bucket } from "./resources";
|
|
1561
|
+
export const bucket = new Bucket({ name: Ref("environment") });
|
|
1562
|
+
`,
|
|
1563
|
+
);
|
|
1564
|
+
|
|
1565
|
+
const result = await tryFoldFile(file, [REF]);
|
|
1566
|
+
|
|
1567
|
+
expect(result.ok).toBe(false);
|
|
1568
|
+
if (result.ok) return;
|
|
1569
|
+
expect(result.reason).toContain(`"Ref" is not a resolvable import`);
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
test("a same-file resource reference passed to an intrinsic call falls back rather than folding to the wrong value", async () => {
|
|
1573
|
+
await writeCallIntrinsicDefs();
|
|
1574
|
+
await writeFile(
|
|
1575
|
+
join(testDir, "resources.ts"),
|
|
1576
|
+
`
|
|
1577
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
1578
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
1579
|
+
`,
|
|
1580
|
+
);
|
|
1581
|
+
const file = join(testDir, "main.ts");
|
|
1582
|
+
await writeFile(
|
|
1583
|
+
file,
|
|
1584
|
+
`
|
|
1585
|
+
import { Bucket } from "./resources";
|
|
1586
|
+
import { Ref } from "./intrinsics";
|
|
1587
|
+
export const source = new Bucket({ name: "src" });
|
|
1588
|
+
export const bucket = new Bucket({ name: Ref(source.arn) });
|
|
1589
|
+
`,
|
|
1590
|
+
);
|
|
1591
|
+
|
|
1592
|
+
const result = await tryFoldFile(file, [REF]);
|
|
1593
|
+
|
|
1594
|
+
expect(result.ok).toBe(false);
|
|
1595
|
+
if (result.ok) return;
|
|
1596
|
+
expect(result.reason).toContain("same-file resource reference");
|
|
1597
|
+
});
|
|
1598
|
+
});
|