@intentius/chant 0.18.19 → 0.18.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +1 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts +18 -2
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/lexicon.d.ts +10 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +10 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts +1 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +4 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/op/builders.d.ts +2 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.test.ts +43 -0
- package/src/build.ts +11 -0
- package/src/cli/handlers/lifecycle.test.ts +42 -0
- package/src/cli/handlers/lifecycle.ts +154 -72
- package/src/components/cli-support.ts +2 -2
- package/src/config.ts +17 -0
- package/src/discovery/collect.test.ts +91 -0
- package/src/discovery/collect.ts +153 -60
- package/src/discovery/index.ts +1 -1
- package/src/lexicon.ts +10 -0
- package/src/lifecycle/git.ts +13 -0
- package/src/lifecycle/snapshot.test.ts +24 -0
- package/src/lifecycle/snapshot.ts +7 -4
- package/src/lifecycle/types.ts +4 -0
- package/src/op/builders.ts +6 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { build } from "../../build";
|
|
3
3
|
import { takeSnapshot } from "../../lifecycle/snapshot";
|
|
4
|
-
import { readSnapshot, readSnapshotAt, readEnvironmentSnapshots, listSnapshots, fetchLifecycle, StaleLifecycleBranchError } from "../../lifecycle/git";
|
|
4
|
+
import { readSnapshot, readSnapshotAt, readEnvironmentSnapshots, listSnapshots, fetchLifecycle, snapshotStorageKey, StaleLifecycleBranchError } from "../../lifecycle/git";
|
|
5
5
|
import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
|
|
6
6
|
import { diffLive, diffLiveArtifacts, diffSnapshots, type LiveDiffResult, type LiveArtifactDiffResult, type SnapshotDiffResult } from "../../lifecycle/live-diff";
|
|
7
7
|
import { buildChangeSet, renderChangeSet, gitlabMrReport, type ChangeSet } from "../../lifecycle/change-set";
|
|
@@ -29,6 +29,34 @@ function resolveBuildRoot(args: ParsedArgs, config: ChantConfig): string {
|
|
|
29
29
|
return resolve(args.src ?? config.sourceDir ?? ".");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** One stack a lifecycle command operates on: its build root and, for a
|
|
33
|
+
* multi-stack project, the deployed CloudFormation stack name it observes
|
|
34
|
+
* against. */
|
|
35
|
+
interface StackTarget {
|
|
36
|
+
/** The deployed stack name to observe (undefined ⇒ single-stack convention:
|
|
37
|
+
* the stack named after the environment). */
|
|
38
|
+
stack?: string;
|
|
39
|
+
/** Build root to synthesize this stack from, scoped so its logical ids match
|
|
40
|
+
* what the stack actually deploys. */
|
|
41
|
+
root: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The stacks a lifecycle command (`snapshot`/`diff`) iterates. A multi-stack
|
|
46
|
+
* project declares them via `stacks` in chant.config (#932) — each built from
|
|
47
|
+
* its own `src` and observed against its own live stack `name`. A single-stack
|
|
48
|
+
* project (no `stacks`) resolves to one target built from `sourceDir`/root and
|
|
49
|
+
* observed as the stack named after the environment (unchanged behavior). An
|
|
50
|
+
* explicit `--src` always wins and forces a single scoped target.
|
|
51
|
+
*/
|
|
52
|
+
function resolveStackTargets(args: ParsedArgs, config: ChantConfig): StackTarget[] {
|
|
53
|
+
if (args.src) return [{ root: resolve(args.src) }];
|
|
54
|
+
if (config.stacks && config.stacks.length > 0) {
|
|
55
|
+
return config.stacks.map((s) => ({ stack: s.name, root: resolve(s.src) }));
|
|
56
|
+
}
|
|
57
|
+
return [{ root: resolveBuildRoot(args, config) }];
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
/**
|
|
33
61
|
* chant lifecycle snapshot <environment> [lexicon]
|
|
34
62
|
*/
|
|
@@ -59,13 +87,6 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
59
87
|
: plugins;
|
|
60
88
|
const targetSerializers = targetPlugins.map((p) => p.serializer);
|
|
61
89
|
|
|
62
|
-
// Build first to get entity names and build output
|
|
63
|
-
const buildResult = await build(resolveBuildRoot(args, config), targetSerializers);
|
|
64
|
-
if (buildResult.errors.length > 0) {
|
|
65
|
-
console.error(formatError({ message: "Build failed — fix errors before taking a snapshot" }));
|
|
66
|
-
return 1;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
90
|
const observingPlugins = targetPlugins.filter((p) => p.describeResources || p.listArtifacts);
|
|
70
91
|
if (observingPlugins.length === 0) {
|
|
71
92
|
console.error(formatError({
|
|
@@ -75,35 +96,55 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
75
96
|
return 1;
|
|
76
97
|
}
|
|
77
98
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
// One target per stack (single-stack projects: exactly one). Each stack builds
|
|
100
|
+
// from its own scoped root — so its logical ids match what it deploys — and
|
|
101
|
+
// snapshots against its own live stack name (#932).
|
|
102
|
+
const targets = resolveStackTargets(args, config);
|
|
103
|
+
let anySnapshotSaved = false;
|
|
104
|
+
let anyHardError = false;
|
|
105
|
+
|
|
106
|
+
for (const target of targets) {
|
|
107
|
+
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
108
|
+
const buildResult = await build(target.root, targetSerializers);
|
|
109
|
+
if (buildResult.errors.length > 0) {
|
|
110
|
+
console.error(formatError({ message: `Build failed for ${label} — fix errors before taking a snapshot` }));
|
|
111
|
+
anyHardError = true;
|
|
112
|
+
continue;
|
|
88
113
|
}
|
|
89
|
-
throw err;
|
|
90
|
-
}
|
|
91
114
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
let result;
|
|
116
|
+
try {
|
|
117
|
+
result = await takeSnapshot(environment, observingPlugins, buildResult, { stack: target.stack });
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err instanceof StaleLifecycleBranchError) {
|
|
120
|
+
console.error(formatError({
|
|
121
|
+
message: `Another snapshot completed for chant/lifecycle after this run started (env: ${environment}).`,
|
|
122
|
+
hint: `Pull and retry: \`git fetch origin ${"chant/lifecycle"}:${"chant/lifecycle"}\` && \`chant lifecycle snapshot ${environment}\`.`,
|
|
123
|
+
}));
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const w of result.warnings) {
|
|
130
|
+
console.error(formatWarning({ message: w }));
|
|
131
|
+
}
|
|
132
|
+
for (const e of result.errors) {
|
|
133
|
+
console.error(formatError({ message: e }));
|
|
134
|
+
}
|
|
98
135
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
|
|
136
|
+
if (result.snapshots.length > 0) {
|
|
137
|
+
anySnapshotSaved = true;
|
|
138
|
+
const prefix = target.stack ? `${target.stack}: ` : "";
|
|
139
|
+
const counts = result.snapshots
|
|
140
|
+
.map((s) => `${s.lexicon}(${Object.keys(s.resources).length})`)
|
|
141
|
+
.join(" ");
|
|
142
|
+
console.error(formatSuccess(`${prefix}Snapshot saved to chant/lifecycle (${counts})`));
|
|
143
|
+
}
|
|
144
|
+
if (result.errors.length > 0) anyHardError = true;
|
|
104
145
|
}
|
|
105
146
|
|
|
106
|
-
return
|
|
147
|
+
return anyHardError && !anySnapshotSaved ? 1 : 0;
|
|
107
148
|
}
|
|
108
149
|
|
|
109
150
|
/**
|
|
@@ -209,26 +250,68 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
209
250
|
? plugins.filter((p) => p.name === lexiconFilter).map((p) => p.serializer)
|
|
210
251
|
: serializers;
|
|
211
252
|
|
|
212
|
-
//
|
|
253
|
+
// Fetch previous snapshots once (all stacks share the orphan branch).
|
|
213
254
|
const { config } = await loadChantConfig(resolve("."));
|
|
214
|
-
const buildResult = await build(resolveBuildRoot(args, config), targetSerializers);
|
|
215
|
-
if (buildResult.errors.length > 0) {
|
|
216
|
-
console.error(formatError({ message: "Build failed — fix errors before diffing" }));
|
|
217
|
-
return 1;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Fetch and read previous snapshot
|
|
221
255
|
await fetchLifecycle();
|
|
222
256
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
257
|
+
// One target per stack (single-stack projects: exactly one), each built from
|
|
258
|
+
// its own scoped root and diffed against its own live stack name (#932).
|
|
259
|
+
const targets = resolveStackTargets(args, config);
|
|
260
|
+
const json = !!args.json;
|
|
261
|
+
const perStackJson: Record<string, unknown> = {};
|
|
262
|
+
let combinedLexiconsJson: Record<string, unknown> | undefined;
|
|
263
|
+
let totalDrift = 0;
|
|
264
|
+
let totalChecked = 0;
|
|
265
|
+
let anyBuildError = false;
|
|
266
|
+
|
|
267
|
+
for (const target of targets) {
|
|
268
|
+
const buildResult = await build(target.root, targetSerializers);
|
|
269
|
+
if (buildResult.errors.length > 0) {
|
|
270
|
+
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
271
|
+
console.error(formatError({ message: `Build failed for ${label} — fix errors before diffing` }));
|
|
272
|
+
anyBuildError = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const lexicons = lexiconFilter
|
|
277
|
+
? [lexiconFilter]
|
|
278
|
+
: Array.from(buildResult.manifest.lexicons);
|
|
279
|
+
|
|
280
|
+
if (args.live) {
|
|
281
|
+
const r = await runLifecycleDiffLive({ environment, lexicons, plugins, buildResult, json, stack: target.stack });
|
|
282
|
+
totalDrift += r.totalDrift;
|
|
283
|
+
totalChecked += r.totalLexiconsChecked;
|
|
284
|
+
if (json) {
|
|
285
|
+
if (target.stack) perStackJson[target.stack] = r.byLexicon;
|
|
286
|
+
else combinedLexiconsJson = r.byLexicon;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
await runLifecycleDiffDigest({ environment, lexicons, buildResult, stack: target.stack });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
226
292
|
|
|
227
293
|
if (args.live) {
|
|
228
|
-
|
|
294
|
+
if (json) {
|
|
295
|
+
// Single-stack keeps the original `{ environment, lexicons }` shape
|
|
296
|
+
// (behold's inspect diff, #852); multi-stack nests under `stacks`.
|
|
297
|
+
console.log(
|
|
298
|
+
JSON.stringify(
|
|
299
|
+
combinedLexiconsJson !== undefined
|
|
300
|
+
? { environment, lexicons: combinedLexiconsJson }
|
|
301
|
+
: { environment, stacks: perStackJson },
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
} else if (totalChecked === 0) {
|
|
305
|
+
console.error(formatWarning({
|
|
306
|
+
message: "No lexicons implement describeResources or listArtifacts — nothing to diff in --live mode",
|
|
307
|
+
}));
|
|
308
|
+
return 1;
|
|
309
|
+
} else if (totalDrift === 0) {
|
|
310
|
+
console.error(formatSuccess(`No drift detected across ${totalChecked} lexicon(s)`));
|
|
311
|
+
}
|
|
229
312
|
}
|
|
230
313
|
|
|
231
|
-
return
|
|
314
|
+
return anyBuildError ? 1 : 0;
|
|
232
315
|
}
|
|
233
316
|
|
|
234
317
|
interface BetweenDiffArgs {
|
|
@@ -300,13 +383,16 @@ interface DigestDiffArgs {
|
|
|
300
383
|
environment: string;
|
|
301
384
|
lexicons: string[];
|
|
302
385
|
buildResult: BuildResult;
|
|
386
|
+
/** Deployed stack name for a multi-stack project (#932); scopes the snapshot read. */
|
|
387
|
+
stack?: string;
|
|
303
388
|
}
|
|
304
389
|
|
|
305
390
|
async function runLifecycleDiffDigest(args: DigestDiffArgs): Promise<number> {
|
|
306
391
|
const currentDigest = computeBuildDigest(args.buildResult);
|
|
392
|
+
if (args.stack) console.log(`\n${formatBold(`■ stack ${args.stack}`)}`);
|
|
307
393
|
|
|
308
394
|
for (const lexicon of args.lexicons) {
|
|
309
|
-
const content = await readSnapshot(args.environment, lexicon);
|
|
395
|
+
const content = await readSnapshot(args.environment, snapshotStorageKey(lexicon, args.stack));
|
|
310
396
|
let previousDigest = undefined;
|
|
311
397
|
if (content) {
|
|
312
398
|
const snapshot: LifecycleSnapshot = JSON.parse(content);
|
|
@@ -343,17 +429,28 @@ interface LiveDiffArgs {
|
|
|
343
429
|
buildResult: BuildResult;
|
|
344
430
|
/** Emit machine-readable JSON on stdout instead of the human report (#852). */
|
|
345
431
|
json: boolean;
|
|
432
|
+
/** Deployed stack name for a multi-stack project (#932); scopes the live
|
|
433
|
+
* observation (which CloudFormation stack to query) and the snapshot read. */
|
|
434
|
+
stack?: string;
|
|
346
435
|
}
|
|
347
436
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
let totalLexiconsChecked = 0;
|
|
351
|
-
// Collected per-lexicon results, emitted as one JSON object when --json is set
|
|
352
|
-
// (parsed by programmatic consumers, e.g. behold's inspect diff — #852).
|
|
353
|
-
const byLexicon: Record<
|
|
437
|
+
interface LiveDiffOutcome {
|
|
438
|
+
byLexicon: Record<
|
|
354
439
|
string,
|
|
355
440
|
{ resources?: LiveDiffResult; observed?: Record<string, ResourceMetadata>; artifacts?: LiveArtifactDiffResult }
|
|
356
|
-
|
|
441
|
+
>;
|
|
442
|
+
totalDrift: number;
|
|
443
|
+
totalLexiconsChecked: number;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Diff current build vs live cloud for one stack. Renders the human report
|
|
447
|
+
* inline; returns per-lexicon results so the caller emits the aggregate `--json`
|
|
448
|
+
* once (single-stack keeps the original shape; multi-stack nests under `stacks`). */
|
|
449
|
+
async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome> {
|
|
450
|
+
let totalDrift = 0;
|
|
451
|
+
let totalLexiconsChecked = 0;
|
|
452
|
+
const byLexicon: LiveDiffOutcome["byLexicon"] = {};
|
|
453
|
+
if (!args.json && args.stack) console.log(`\n${formatBold(`■ stack ${args.stack}`)}`);
|
|
357
454
|
|
|
358
455
|
for (const lexiconName of args.lexicons) {
|
|
359
456
|
const plugin = args.plugins.find((p) => p.name === lexiconName);
|
|
@@ -391,7 +488,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<number> {
|
|
|
391
488
|
|
|
392
489
|
// Read previous snapshot once; both flows pull what they need.
|
|
393
490
|
let prevSnapshot: LifecycleSnapshot | undefined;
|
|
394
|
-
const content = await readSnapshot(args.environment, lexiconName);
|
|
491
|
+
const content = await readSnapshot(args.environment, snapshotStorageKey(lexiconName, args.stack));
|
|
395
492
|
if (content) prevSnapshot = JSON.parse(content);
|
|
396
493
|
|
|
397
494
|
let lexiconChecked = false;
|
|
@@ -405,6 +502,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<number> {
|
|
|
405
502
|
buildOutput,
|
|
406
503
|
entityNames: Array.from(declared),
|
|
407
504
|
entities,
|
|
505
|
+
stack: args.stack,
|
|
408
506
|
});
|
|
409
507
|
} catch (err) {
|
|
410
508
|
console.error(formatError({
|
|
@@ -427,7 +525,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<number> {
|
|
|
427
525
|
if (plugin.listArtifacts) {
|
|
428
526
|
let observedNow: Record<string, ArtifactMetadata>;
|
|
429
527
|
try {
|
|
430
|
-
observedNow = await plugin.listArtifacts({ environment: args.environment, entities });
|
|
528
|
+
observedNow = await plugin.listArtifacts({ environment: args.environment, entities, stack: args.stack });
|
|
431
529
|
} catch (err) {
|
|
432
530
|
console.error(formatError({
|
|
433
531
|
message: `${lexiconName}: listArtifacts failed — ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -445,23 +543,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<number> {
|
|
|
445
543
|
if (lexiconChecked) totalLexiconsChecked++;
|
|
446
544
|
}
|
|
447
545
|
|
|
448
|
-
|
|
449
|
-
console.error(formatWarning({
|
|
450
|
-
message: "No lexicons implement describeResources or listArtifacts — nothing to diff in --live mode",
|
|
451
|
-
}));
|
|
452
|
-
return 1;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (args.json) {
|
|
456
|
-
console.log(JSON.stringify({ environment: args.environment, lexicons: byLexicon }));
|
|
457
|
-
return 0;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (totalDrift === 0) {
|
|
461
|
-
console.error(formatSuccess(`No drift detected across ${totalLexiconsChecked} lexicon(s)`));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return 0;
|
|
546
|
+
return { byLexicon, totalDrift, totalLexiconsChecked };
|
|
465
547
|
}
|
|
466
548
|
|
|
467
549
|
function renderLiveDiff(lexiconName: string, environment: string, diff: LiveDiffResult): void {
|
|
@@ -160,7 +160,7 @@ export async function computeComponentGraph(path: string): Promise<ComponentGrap
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
/** A generate-mode target lexicon name — any lexicon whose plugin implements `generateComponentPipeline` (
|
|
163
|
+
/** A generate-mode target lexicon name — any lexicon whose plugin implements `generateComponentPipeline` (gitlab, github, and forgejo today). */
|
|
164
164
|
export type GenerateLexicon = string;
|
|
165
165
|
|
|
166
166
|
/** Result of `chant build --components --generate <lexicon>`. */
|
|
@@ -213,7 +213,7 @@ export async function generateComponentsPipeline(
|
|
|
213
213
|
if (!plugin?.generateComponentPipeline) {
|
|
214
214
|
return {
|
|
215
215
|
success: false,
|
|
216
|
-
error: `Lexicon "${lexicon}" does not support generate mode (no generateComponentPipeline). GitLab
|
|
216
|
+
error: `Lexicon "${lexicon}" does not support generate mode (no generateComponentPipeline). GitLab, GitHub, and Forgejo are supported today.`,
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
219
|
|
package/src/config.ts
CHANGED
|
@@ -82,6 +82,23 @@ export interface ChantConfig {
|
|
|
82
82
|
*/
|
|
83
83
|
sourceDir?: string;
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Multi-stack projects: the independently-deployed CloudFormation stacks this
|
|
87
|
+
* project comprises, each built from its own source directory. When set,
|
|
88
|
+
* lifecycle commands (`snapshot`/`diff`) iterate every stack — building each
|
|
89
|
+
* `src` scoped (so its logical ids match what that stack actually deploys) and
|
|
90
|
+
* observing it against its own live stack `name` — instead of assuming one
|
|
91
|
+
* stack per environment. Leave unset for a single-stack project (the default:
|
|
92
|
+
* one build from `sourceDir`/root, observed as the stack named after the
|
|
93
|
+
* environment). See {@link resolveStackTargets}.
|
|
94
|
+
*/
|
|
95
|
+
stacks?: Array<{
|
|
96
|
+
/** The deployed CloudFormation stack name (what `cfn-deploy` targets). */
|
|
97
|
+
name: string;
|
|
98
|
+
/** Source directory to build for this stack, relative to the project root. */
|
|
99
|
+
src: string;
|
|
100
|
+
}>;
|
|
101
|
+
|
|
85
102
|
/** Lint configuration (rules, extends, overrides, plugins) */
|
|
86
103
|
lint?: LintConfig;
|
|
87
104
|
|
|
@@ -280,6 +280,97 @@ describe("collectEntities", () => {
|
|
|
280
280
|
});
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
+
// #932 — a multi-stack project (independently-deployed sibling stacks under one
|
|
284
|
+
// root) legitimately reuses conventional cross-stack Parameter names across
|
|
285
|
+
// siblings. The unscoped whole-project build must namespace by directory instead
|
|
286
|
+
// of throwing, while a per-stack scoped build keeps the raw (deployed) names.
|
|
287
|
+
describe("collectEntities — cross-directory namespaces (#932)", () => {
|
|
288
|
+
test("the same bare name in two different directories is disambiguated by a stack prefix, not thrown", () => {
|
|
289
|
+
const agentsBucket = createMockEntity("param");
|
|
290
|
+
const backendBucket = createMockEntity("param");
|
|
291
|
+
|
|
292
|
+
const result = collectEntities(
|
|
293
|
+
[
|
|
294
|
+
{ file: "src/loom-agents/params.ts", exports: { pArtifactBucket: agentsBucket } },
|
|
295
|
+
{ file: "src/loom-backend/params.ts", exports: { pArtifactBucket: backendBucket } },
|
|
296
|
+
],
|
|
297
|
+
"src",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(result.size).toBe(2);
|
|
301
|
+
expect(result.get("LoomAgentspArtifactBucket")).toBe(agentsBucket);
|
|
302
|
+
expect(result.get("LoomBackendpArtifactBucket")).toBe(backendBucket);
|
|
303
|
+
// The raw name is not used when it collides across directories.
|
|
304
|
+
expect(result.has("pArtifactBucket")).toBe(false);
|
|
305
|
+
// Disambiguated keys stay within CloudFormation's logical-id grammar.
|
|
306
|
+
for (const key of result.keys()) expect(key).toMatch(/^[A-Za-z0-9]+$/);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("a genuine same-directory duplicate still throws", async () => {
|
|
310
|
+
const a = createMockEntity("param");
|
|
311
|
+
const b = createMockEntity("param");
|
|
312
|
+
|
|
313
|
+
await expectToThrow(
|
|
314
|
+
() =>
|
|
315
|
+
collectEntities(
|
|
316
|
+
[
|
|
317
|
+
{ file: "src/loom-backend/params.ts", exports: { pArtifactBucket: a } },
|
|
318
|
+
{ file: "src/loom-backend/more.ts", exports: { pArtifactBucket: b } },
|
|
319
|
+
],
|
|
320
|
+
"src",
|
|
321
|
+
),
|
|
322
|
+
DiscoveryError,
|
|
323
|
+
(error) => {
|
|
324
|
+
expect(error.type).toBe("resolution");
|
|
325
|
+
expect(error.message).toBe('Duplicate export name "pArtifactBucket" found');
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("the same object re-exported from two directories stays one raw-named entity", () => {
|
|
331
|
+
const shared = createMockEntity("param");
|
|
332
|
+
|
|
333
|
+
const result = collectEntities(
|
|
334
|
+
[
|
|
335
|
+
{ file: "src/a/x.ts", exports: { shared } },
|
|
336
|
+
{ file: "src/b/y.ts", exports: { shared } },
|
|
337
|
+
],
|
|
338
|
+
"src",
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(result.size).toBe(1);
|
|
342
|
+
expect(result.get("shared")).toBe(shared);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("non-colliding names in different directories keep their raw names (single-stack subdirs unaffected)", () => {
|
|
346
|
+
const vpc = createMockEntity("resource");
|
|
347
|
+
const app = createMockEntity("resource");
|
|
348
|
+
|
|
349
|
+
const result = collectEntities(
|
|
350
|
+
[
|
|
351
|
+
{ file: "src/network/vpc.ts", exports: { vpc } },
|
|
352
|
+
{ file: "src/compute/app.ts", exports: { app } },
|
|
353
|
+
],
|
|
354
|
+
"src",
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
expect(result.size).toBe(2);
|
|
358
|
+
expect(result.get("vpc")).toBe(vpc);
|
|
359
|
+
expect(result.get("app")).toBe(app);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a scoped per-stack build (buildRoot == the stack dir) keeps raw names — deploy fidelity", () => {
|
|
363
|
+
const bucket = createMockEntity("param");
|
|
364
|
+
|
|
365
|
+
const result = collectEntities(
|
|
366
|
+
[{ file: "src/loom-backend/params.ts", exports: { pArtifactBucket: bucket } }],
|
|
367
|
+
"src/loom-backend",
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(result.get("pArtifactBucket")).toBe(bucket);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
283
374
|
describe("collectEntities with composites", () => {
|
|
284
375
|
beforeEach(() => {
|
|
285
376
|
CompositeRegistry.clear();
|