@intentius/chant 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/build.test.ts +30 -3
- package/src/build.ts +16 -6
- package/src/lexicon-output.test.ts +21 -0
- package/src/lexicon-output.ts +51 -18
package/package.json
CHANGED
package/src/build.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
|
2
2
|
import { build, partitionByLexicon, detectCrossLexiconRefs } from "./build";
|
|
3
3
|
import { output } from "./lexicon-output";
|
|
4
4
|
import { AttrRef } from "./attrref";
|
|
5
|
+
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
5
6
|
import type { Serializer } from "./serializer";
|
|
6
7
|
import type { Declarable } from "./declarable";
|
|
7
8
|
import { DECLARABLE_MARKER } from "./declarable";
|
|
@@ -359,14 +360,14 @@ describe("detectCrossLexiconRefs", () => {
|
|
|
359
360
|
expect(explicitOutputs[0].outputName).toBe("MyCustomArnName");
|
|
360
361
|
|
|
361
362
|
// Merge logic: explicit wins (same parent object + attribute)
|
|
362
|
-
const explicitRefs = explicitOutputs.map((o: { _sourceParent: WeakRef<object
|
|
363
|
-
parent: o._sourceParent
|
|
363
|
+
const explicitRefs = explicitOutputs.map((o: { _sourceParent: WeakRef<object> | null; sourceAttribute: string | null }) => ({
|
|
364
|
+
parent: o._sourceParent?.deref(),
|
|
364
365
|
attribute: o.sourceAttribute,
|
|
365
366
|
}));
|
|
366
367
|
const merged = [
|
|
367
368
|
...explicitOutputs,
|
|
368
369
|
...autoDetected.filter((auto) => {
|
|
369
|
-
const autoParent = auto._sourceParent
|
|
370
|
+
const autoParent = auto._sourceParent?.deref();
|
|
370
371
|
return !explicitRefs.some(
|
|
371
372
|
(e: { parent: object | undefined; attribute: string }) =>
|
|
372
373
|
e.parent === autoParent && e.attribute === auto.sourceAttribute
|
|
@@ -403,6 +404,32 @@ describe("detectCrossLexiconRefs", () => {
|
|
|
403
404
|
expect(detected).toHaveLength(0);
|
|
404
405
|
});
|
|
405
406
|
|
|
407
|
+
test("intrinsic-based output() is passed to every lexicon's serializer", () => {
|
|
408
|
+
const alphaEntity = {
|
|
409
|
+
lexicon: "alpha",
|
|
410
|
+
entityType: "Alpha::Resource",
|
|
411
|
+
[DECLARABLE_MARKER]: true,
|
|
412
|
+
} as Declarable;
|
|
413
|
+
|
|
414
|
+
const mockIntrinsic = {
|
|
415
|
+
[INTRINSIC_MARKER]: true as const,
|
|
416
|
+
toJSON: () => ({ "Fn::Sub": "http://example.com/path" }),
|
|
417
|
+
};
|
|
418
|
+
const intrinsicOutput = output(mockIntrinsic, "MyUrl");
|
|
419
|
+
|
|
420
|
+
const entities = new Map<string, Declarable>([
|
|
421
|
+
["alphaEntity", alphaEntity],
|
|
422
|
+
["myUrl", intrinsicOutput as unknown as Declarable],
|
|
423
|
+
]);
|
|
424
|
+
|
|
425
|
+
const { collectLexiconOutputs } = require("./build");
|
|
426
|
+
const collected = collectLexiconOutputs(entities);
|
|
427
|
+
expect(collected).toHaveLength(1);
|
|
428
|
+
expect(collected[0].outputName).toBe("MyUrl");
|
|
429
|
+
expect(collected[0].sourceLexicon).toBe("");
|
|
430
|
+
expect(collected[0].getOutputValue()).toEqual({ "Fn::Sub": "http://example.com/path" });
|
|
431
|
+
});
|
|
432
|
+
|
|
406
433
|
test("deduplicates when same cross-lexicon ref appears in multiple entities", () => {
|
|
407
434
|
const alphaBucket = {
|
|
408
435
|
lexicon: "alpha",
|
package/src/build.ts
CHANGED
|
@@ -105,7 +105,7 @@ export function collectLexiconOutputs(
|
|
|
105
105
|
if (isLexiconOutput(entity as unknown)) {
|
|
106
106
|
const lexiconOutput = entity as unknown as LexiconOutput;
|
|
107
107
|
// Resolve source entity name from the WeakRef parent identity
|
|
108
|
-
const parent = lexiconOutput._sourceParent
|
|
108
|
+
const parent = lexiconOutput._sourceParent?.deref();
|
|
109
109
|
let sourceName = name;
|
|
110
110
|
if (parent) {
|
|
111
111
|
for (const [entityName, e] of entities) {
|
|
@@ -287,7 +287,7 @@ function generateManifest(
|
|
|
287
287
|
outputsRecord[output.outputName] = {
|
|
288
288
|
source: output.sourceLexicon,
|
|
289
289
|
entity: output.sourceEntity,
|
|
290
|
-
attribute: output.sourceAttribute,
|
|
290
|
+
attribute: output.sourceAttribute ?? "",
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
293
|
|
|
@@ -391,22 +391,29 @@ export async function build(
|
|
|
391
391
|
// Merge: explicit outputs take precedence over auto-detected ones.
|
|
392
392
|
// Match by parent object identity + attribute to detect collisions.
|
|
393
393
|
const explicitRefs = explicitOutputs.map((o) => ({
|
|
394
|
-
parent: o._sourceParent
|
|
394
|
+
parent: o._sourceParent?.deref(),
|
|
395
395
|
attribute: o.sourceAttribute,
|
|
396
396
|
}));
|
|
397
397
|
const lexiconOutputs = [
|
|
398
398
|
...explicitOutputs,
|
|
399
399
|
...autoOutputs.filter((auto) => {
|
|
400
|
-
const autoParent = auto._sourceParent
|
|
400
|
+
const autoParent = auto._sourceParent?.deref();
|
|
401
401
|
return !explicitRefs.some(
|
|
402
402
|
(e) => e.parent === autoParent && e.attribute === auto.sourceAttribute
|
|
403
403
|
);
|
|
404
404
|
}),
|
|
405
405
|
];
|
|
406
406
|
|
|
407
|
-
// Group outputs by source lexicon
|
|
407
|
+
// Group outputs by source lexicon.
|
|
408
|
+
// Intrinsic-based outputs (sourceLexicon === "") have no source entity to derive a lexicon from,
|
|
409
|
+
// so they are included in every lexicon's output list.
|
|
408
410
|
const outputsByLexicon = new Map<string, LexiconOutput[]>();
|
|
411
|
+
const unassignedOutputs: LexiconOutput[] = [];
|
|
409
412
|
for (const output of lexiconOutputs) {
|
|
413
|
+
if (!output.sourceLexicon) {
|
|
414
|
+
unassignedOutputs.push(output);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
410
417
|
if (!outputsByLexicon.has(output.sourceLexicon)) {
|
|
411
418
|
outputsByLexicon.set(output.sourceLexicon, []);
|
|
412
419
|
}
|
|
@@ -418,7 +425,10 @@ export async function build(
|
|
|
418
425
|
for (const [lexiconName, lexiconEntities] of partitions) {
|
|
419
426
|
const serializer = serializersByName.get(lexiconName);
|
|
420
427
|
if (serializer) {
|
|
421
|
-
const lexiconLexiconOutputs =
|
|
428
|
+
const lexiconLexiconOutputs = [
|
|
429
|
+
...(outputsByLexicon.get(lexiconName) ?? []),
|
|
430
|
+
...unassignedOutputs,
|
|
431
|
+
];
|
|
422
432
|
outputs.set(lexiconName, serializer.serialize(lexiconEntities, lexiconLexiconOutputs));
|
|
423
433
|
} else {
|
|
424
434
|
warnings.push(`No serializer found for lexicon "${lexiconName}"`);
|
|
@@ -71,6 +71,27 @@ describe("LexiconOutput", () => {
|
|
|
71
71
|
|
|
72
72
|
expect(() => new LexiconOutput(ref, "Test")).toThrow("no lexicon field");
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
test("accepts an Intrinsic and sets sourceAttribute to null", () => {
|
|
76
|
+
const mockIntrinsic = { [INTRINSIC_MARKER]: true as const, toJSON: () => ({ "Fn::Sub": "hello" }) };
|
|
77
|
+
const lo = new LexiconOutput(mockIntrinsic, "MyOutput");
|
|
78
|
+
expect(lo.sourceAttribute).toBeNull();
|
|
79
|
+
expect(lo.sourceLexicon).toBe("");
|
|
80
|
+
expect(lo.outputName).toBe("MyOutput");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("getOutputValue() returns Fn::GetAtt for AttrRef-based output", () => {
|
|
84
|
+
const bucket = new MockResource();
|
|
85
|
+
const lo = new LexiconOutput(bucket.arn, "BucketArn");
|
|
86
|
+
lo._setSourceEntity("myBucket");
|
|
87
|
+
expect(lo.getOutputValue()).toEqual({ "Fn::GetAtt": ["myBucket", "Arn"] });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("getOutputValue() returns intrinsic toJSON for Intrinsic-based output", () => {
|
|
91
|
+
const mockIntrinsic = { [INTRINSIC_MARKER]: true as const, toJSON: () => ({ "Fn::Sub": "http://${Param}/path" }) };
|
|
92
|
+
const lo = new LexiconOutput(mockIntrinsic, "MyUrl");
|
|
93
|
+
expect(lo.getOutputValue()).toEqual({ "Fn::Sub": "http://${Param}/path" });
|
|
94
|
+
});
|
|
74
95
|
});
|
|
75
96
|
|
|
76
97
|
describe("LexiconOutput.auto", () => {
|
package/src/lexicon-output.ts
CHANGED
|
@@ -6,31 +6,47 @@ import { AttrRef } from "./attrref";
|
|
|
6
6
|
* with any consuming lexicon (e.g. GitHub, Cloudflare).
|
|
7
7
|
*
|
|
8
8
|
* Implements Intrinsic so it can be used as Value<string> anywhere.
|
|
9
|
+
*
|
|
10
|
+
* Accepts either an AttrRef (resource attribute reference) or any Intrinsic
|
|
11
|
+
* (e.g. Sub, Join) for computed output values like constructed URLs.
|
|
9
12
|
*/
|
|
10
13
|
export class LexiconOutput implements Intrinsic {
|
|
11
14
|
readonly [INTRINSIC_MARKER] = true as const;
|
|
12
15
|
readonly sourceLexicon: string;
|
|
13
16
|
readonly sourceEntity: string;
|
|
14
|
-
readonly sourceAttribute: string;
|
|
17
|
+
readonly sourceAttribute: string | null;
|
|
15
18
|
readonly outputName: string;
|
|
16
19
|
/** @internal WeakRef to the source entity object for identity-based matching */
|
|
17
|
-
readonly _sourceParent: WeakRef<object
|
|
20
|
+
readonly _sourceParent: WeakRef<object> | null;
|
|
21
|
+
/** @internal Intrinsic value when constructed from an Intrinsic rather than AttrRef */
|
|
22
|
+
private readonly _intrinsic: Intrinsic | null;
|
|
18
23
|
|
|
19
|
-
constructor(ref: AttrRef, name: string) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
constructor(ref: AttrRef | Intrinsic, name: string) {
|
|
25
|
+
if (ref instanceof AttrRef) {
|
|
26
|
+
const parent = ref.parent.deref();
|
|
27
|
+
if (!parent) {
|
|
28
|
+
throw new Error("Cannot create LexiconOutput: parent entity has been garbage collected");
|
|
29
|
+
}
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if (!("lexicon" in parent) || typeof (parent as Record<string, unknown>).lexicon !== "string") {
|
|
32
|
+
throw new Error("Cannot create LexiconOutput: parent entity has no lexicon field");
|
|
33
|
+
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
this.sourceLexicon = (parent as Record<string, unknown>).lexicon as string;
|
|
36
|
+
this.sourceEntity = "";
|
|
37
|
+
this.sourceAttribute = ref.attribute;
|
|
38
|
+
this.outputName = name;
|
|
39
|
+
this._sourceParent = ref.parent;
|
|
40
|
+
this._intrinsic = null;
|
|
41
|
+
} else {
|
|
42
|
+
// Intrinsic (Sub, Join, Ref, etc.) — no parent entity tracking needed
|
|
43
|
+
this.sourceLexicon = "";
|
|
44
|
+
this.sourceEntity = "";
|
|
45
|
+
this.sourceAttribute = null;
|
|
46
|
+
this.outputName = name;
|
|
47
|
+
this._sourceParent = null;
|
|
48
|
+
this._intrinsic = ref;
|
|
49
|
+
}
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
/**
|
|
@@ -42,6 +58,18 @@ export class LexiconOutput implements Intrinsic {
|
|
|
42
58
|
(this as { sourceEntity: string }).sourceEntity = name;
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Returns the CloudFormation Output Value for this output.
|
|
63
|
+
* For AttrRef-based outputs: emits Fn::GetAtt.
|
|
64
|
+
* For Intrinsic-based outputs: delegates to the intrinsic's toJSON().
|
|
65
|
+
*/
|
|
66
|
+
getOutputValue(): unknown {
|
|
67
|
+
if (this._intrinsic) {
|
|
68
|
+
return this._intrinsic.toJSON();
|
|
69
|
+
}
|
|
70
|
+
return { "Fn::GetAtt": [this.sourceEntity, this.sourceAttribute] };
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
/**
|
|
46
74
|
* Create a LexiconOutput with an auto-generated name from entity name and attribute.
|
|
47
75
|
* Used during cross-lexicon ref auto-detection.
|
|
@@ -63,14 +91,19 @@ export class LexiconOutput implements Intrinsic {
|
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
/**
|
|
66
|
-
* Create a LexiconOutput from an AttrRef and a user-provided output name.
|
|
94
|
+
* Create a LexiconOutput from an AttrRef or Intrinsic and a user-provided output name.
|
|
67
95
|
*
|
|
68
|
-
* Usage:
|
|
96
|
+
* Usage with AttrRef:
|
|
69
97
|
* ```ts
|
|
70
98
|
* const bucketArn = output(dataBucket.arn, "DataBucketArn");
|
|
71
99
|
* ```
|
|
100
|
+
*
|
|
101
|
+
* Usage with an intrinsic (e.g. a constructed URL):
|
|
102
|
+
* ```ts
|
|
103
|
+
* const solrUrl = output(Sub`http://${Ref(albDnsName)}/solr`, "solrUrl");
|
|
104
|
+
* ```
|
|
72
105
|
*/
|
|
73
|
-
export function output(ref: AttrRef, name: string): LexiconOutput {
|
|
106
|
+
export function output(ref: AttrRef | Intrinsic, name: string): LexiconOutput {
|
|
74
107
|
return new LexiconOutput(ref, name);
|
|
75
108
|
}
|
|
76
109
|
|