@intentius/chant 0.18.14 → 0.18.16
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/lexicon-output.d.ts +21 -1
- package/dist/lexicon-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.test.ts +41 -3
- package/src/lexicon-output.test.ts +34 -3
- package/src/lexicon-output.ts +34 -2
package/dist/lexicon-output.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { INTRINSIC_MARKER, type Intrinsic } from "./intrinsic.js";
|
|
2
2
|
import { AttrRef } from "./attrref.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sanitize auto-generated Output name parts into a valid CloudFormation
|
|
5
|
+
* logical id. Real CloudFormation logical ids (including `Outputs` keys)
|
|
6
|
+
* must match `^[A-Za-z0-9]+$` — no dots, underscores, or other punctuation.
|
|
7
|
+
*
|
|
8
|
+
* Splits every part on runs of non-alphanumeric characters (dots from a
|
|
9
|
+
* nested `Fn::GetAtt` attribute path, underscores, hyphens, etc.) and
|
|
10
|
+
* re-joins the resulting segments in camelCase. This keeps the id
|
|
11
|
+
* deterministic and unique per (entityName, attribute) pair while
|
|
12
|
+
* discarding only punctuation — no information is lost, so two distinct
|
|
13
|
+
* inputs only collide if they already differ solely by punctuation.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function sanitizeLogicalId(...parts: string[]): string;
|
|
3
18
|
/**
|
|
4
19
|
* Represents a cross-lexicon output that bridges a producing lexicon (e.g. AWS)
|
|
5
20
|
* with any consuming lexicon (e.g. GitHub, Cloudflare).
|
|
@@ -36,9 +51,14 @@ export declare class LexiconOutput implements Intrinsic {
|
|
|
36
51
|
* Create a LexiconOutput with an auto-generated name from entity name and attribute.
|
|
37
52
|
* Used during cross-lexicon ref auto-detection.
|
|
38
53
|
*
|
|
54
|
+
* The name is sanitized to a valid CloudFormation logical id (see
|
|
55
|
+
* {@link sanitizeLogicalId}) since it is used verbatim as the `Outputs`
|
|
56
|
+
* key by lexicon serializers (e.g. AWS CloudFormation), which reject
|
|
57
|
+
* dots or underscores in logical ids.
|
|
58
|
+
*
|
|
39
59
|
* @param ref - The AttrRef pointing to the source entity
|
|
40
60
|
* @param entityName - The logical name of the source entity
|
|
41
|
-
* @returns A LexiconOutput with name `{entityName}
|
|
61
|
+
* @returns A LexiconOutput with a name derived from `{entityName}{Attribute}`
|
|
42
62
|
*/
|
|
43
63
|
static auto(ref: AttrRef, entityName: string): LexiconOutput;
|
|
44
64
|
toJSON(): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexicon-output.d.ts","sourceRoot":"","sources":["../src/lexicon-output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;;;;;;;GAQG;AACH,qBAAa,aAAc,YAAW,SAAS;IAC7C,QAAQ,CAAC,CAAC,gBAAgB,CAAC,EAAG,IAAI,CAAU;IAC5C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC/C,uFAAuF;IACvF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;gBAElC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM;IA8B3D;;;;OAIG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpC;;;;OAIG;IACH,cAAc,IAAI,OAAO;IAOzB
|
|
1
|
+
{"version":3,"file":"lexicon-output.d.ts","sourceRoot":"","sources":["../src/lexicon-output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAW5D;AAED;;;;;;;;GAQG;AACH,qBAAa,aAAc,YAAW,SAAS;IAC7C,QAAQ,CAAC,CAAC,gBAAgB,CAAC,EAAG,IAAI,CAAU;IAC5C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC/C,uFAAuF;IACvF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;gBAElC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM;IA8B3D;;;;OAIG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpC;;;;OAIG;IACH,cAAc,IAAI,OAAO;IAOzB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa;IAO5D,MAAM,IAAI;QAAE,eAAe,EAAE,MAAM,CAAA;KAAE;CAGtC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAErF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE"}
|
package/package.json
CHANGED
package/src/build.test.ts
CHANGED
|
@@ -323,7 +323,8 @@ describe("detectCrossLexiconRefs", () => {
|
|
|
323
323
|
expect(detected[0].sourceLexicon).toBe("alpha");
|
|
324
324
|
expect(detected[0].sourceEntity).toBe("dataBucket");
|
|
325
325
|
expect(detected[0].sourceAttribute).toBe("Endpoint");
|
|
326
|
-
expect(detected[0].outputName).toBe("
|
|
326
|
+
expect(detected[0].outputName).toBe("dataBucketEndpoint");
|
|
327
|
+
expect(detected[0].outputName).toMatch(/^[A-Za-z0-9]+$/);
|
|
327
328
|
});
|
|
328
329
|
|
|
329
330
|
test("explicit output() overrides auto-detected name", () => {
|
|
@@ -351,7 +352,7 @@ describe("detectCrossLexiconRefs", () => {
|
|
|
351
352
|
// Auto-detect finds the cross-lexicon ref
|
|
352
353
|
const autoDetected = detectCrossLexiconRefs(entities);
|
|
353
354
|
expect(autoDetected).toHaveLength(1);
|
|
354
|
-
expect(autoDetected[0].outputName).toBe("
|
|
355
|
+
expect(autoDetected[0].outputName).toBe("dataBucketArn");
|
|
355
356
|
|
|
356
357
|
// But when collecting explicit outputs, the explicit one is found
|
|
357
358
|
const explicitOutputs = collectLexiconOutputs(entities);
|
|
@@ -459,7 +460,44 @@ describe("detectCrossLexiconRefs", () => {
|
|
|
459
460
|
|
|
460
461
|
const detected = detectCrossLexiconRefs(entities);
|
|
461
462
|
expect(detected).toHaveLength(1);
|
|
462
|
-
expect(detected[0].outputName).toBe("
|
|
463
|
+
expect(detected[0].outputName).toBe("dataBucketEndpoint");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// chant#930 — a nested Fn::GetAtt attribute path (dots) on a cross-lexicon
|
|
467
|
+
// ref must not leak into the auto-generated Output logical id: CFN logical
|
|
468
|
+
// ids (including Outputs keys) are alphanumeric-only (^[A-Za-z0-9]+$), and
|
|
469
|
+
// cfn-lint (E6001) rejects anything else.
|
|
470
|
+
test("auto-generated Output logical id is valid CFN form for a nested attribute path", () => {
|
|
471
|
+
const alphaBucket = {
|
|
472
|
+
lexicon: "alpha",
|
|
473
|
+
entityType: "Alpha::Storage::Bucket",
|
|
474
|
+
[DECLARABLE_MARKER]: true,
|
|
475
|
+
} as Declarable;
|
|
476
|
+
|
|
477
|
+
const nestedRef = new AttrRef(
|
|
478
|
+
alphaBucket,
|
|
479
|
+
"MetadataConfiguration.AnnotationTableConfiguration.TableArn"
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const ghAction = {
|
|
483
|
+
lexicon: "github",
|
|
484
|
+
entityType: "Action",
|
|
485
|
+
[DECLARABLE_MARKER]: true,
|
|
486
|
+
props: { arn: nestedRef },
|
|
487
|
+
} as unknown as Declarable;
|
|
488
|
+
|
|
489
|
+
const entities = new Map<string, Declarable>([
|
|
490
|
+
["foundationArtifactBucket", alphaBucket],
|
|
491
|
+
["deployAction", ghAction],
|
|
492
|
+
]);
|
|
493
|
+
|
|
494
|
+
const detected = detectCrossLexiconRefs(entities);
|
|
495
|
+
|
|
496
|
+
expect(detected).toHaveLength(1);
|
|
497
|
+
expect(detected[0].outputName).toMatch(/^[A-Za-z0-9]+$/);
|
|
498
|
+
expect(detected[0].outputName).toBe(
|
|
499
|
+
"foundationArtifactBucketMetadataConfigurationAnnotationTableConfigurationTableArn"
|
|
500
|
+
);
|
|
463
501
|
});
|
|
464
502
|
});
|
|
465
503
|
|
|
@@ -100,18 +100,18 @@ describe("LexiconOutput.auto", () => {
|
|
|
100
100
|
const result = LexiconOutput.auto(bucket.arn, "dataBucket");
|
|
101
101
|
|
|
102
102
|
expect(result).toBeInstanceOf(LexiconOutput);
|
|
103
|
-
expect(result.outputName).toBe("
|
|
103
|
+
expect(result.outputName).toBe("dataBucketArn");
|
|
104
104
|
expect(result.sourceLexicon).toBe("testdom");
|
|
105
105
|
expect(result.sourceEntity).toBe("dataBucket");
|
|
106
106
|
expect(result.sourceAttribute).toBe("Arn");
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
test("auto-generated name follows {entityName}
|
|
109
|
+
test("auto-generated name follows {entityName}{Attribute} camelCase pattern", () => {
|
|
110
110
|
const bucket = new MockResource();
|
|
111
111
|
const endpointRef = new AttrRef(bucket, "Endpoint");
|
|
112
112
|
const result = LexiconOutput.auto(endpointRef, "myBucket");
|
|
113
113
|
|
|
114
|
-
expect(result.outputName).toBe("
|
|
114
|
+
expect(result.outputName).toBe("myBucketEndpoint");
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
test("sets sourceEntity immediately", () => {
|
|
@@ -121,6 +121,37 @@ describe("LexiconOutput.auto", () => {
|
|
|
121
121
|
// sourceEntity should be set right away, not empty
|
|
122
122
|
expect(result.sourceEntity).toBe("logsBucket");
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
// chant#930 — CFN logical ids (including Outputs keys) must be alphanumeric
|
|
126
|
+
// only. A nested Fn::GetAtt attribute path (dots) must not leak through.
|
|
127
|
+
test("sanitizes dotted nested-attribute paths to a valid CFN logical id", () => {
|
|
128
|
+
const bucket = new MockResource();
|
|
129
|
+
const nestedRef = new AttrRef(
|
|
130
|
+
bucket,
|
|
131
|
+
"MetadataConfiguration.AnnotationTableConfiguration.TableArn"
|
|
132
|
+
);
|
|
133
|
+
const result = LexiconOutput.auto(nestedRef, "foundationArtifactBucket");
|
|
134
|
+
|
|
135
|
+
expect(result.outputName).toMatch(/^[A-Za-z0-9]+$/);
|
|
136
|
+
expect(result.outputName).toBe(
|
|
137
|
+
"foundationArtifactBucketMetadataConfigurationAnnotationTableConfigurationTableArn"
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("auto-generated output names are always valid CFN logical ids", () => {
|
|
142
|
+
const bucket = new MockResource();
|
|
143
|
+
const cases: Array<[string, string]> = [
|
|
144
|
+
["dataBucket", "Arn"],
|
|
145
|
+
["my-bucket_2", "Some.Nested.Attr"],
|
|
146
|
+
["logsBucket", "Endpoint.Hostname"],
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
for (const [entityName, attribute] of cases) {
|
|
150
|
+
const ref = new AttrRef(bucket, attribute);
|
|
151
|
+
const result = LexiconOutput.auto(ref, entityName);
|
|
152
|
+
expect(result.outputName).toMatch(/^[A-Za-z0-9]+$/);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
124
155
|
});
|
|
125
156
|
|
|
126
157
|
describe("output() helper", () => {
|
package/src/lexicon-output.ts
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { INTRINSIC_MARKER, type Intrinsic } from "./intrinsic";
|
|
2
2
|
import { AttrRef } from "./attrref";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize auto-generated Output name parts into a valid CloudFormation
|
|
6
|
+
* logical id. Real CloudFormation logical ids (including `Outputs` keys)
|
|
7
|
+
* must match `^[A-Za-z0-9]+$` — no dots, underscores, or other punctuation.
|
|
8
|
+
*
|
|
9
|
+
* Splits every part on runs of non-alphanumeric characters (dots from a
|
|
10
|
+
* nested `Fn::GetAtt` attribute path, underscores, hyphens, etc.) and
|
|
11
|
+
* re-joins the resulting segments in camelCase. This keeps the id
|
|
12
|
+
* deterministic and unique per (entityName, attribute) pair while
|
|
13
|
+
* discarding only punctuation — no information is lost, so two distinct
|
|
14
|
+
* inputs only collide if they already differ solely by punctuation.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeLogicalId(...parts: string[]): string {
|
|
19
|
+
const segments = parts
|
|
20
|
+
.join("_")
|
|
21
|
+
.split(/[^A-Za-z0-9]+/)
|
|
22
|
+
.filter((segment) => segment.length > 0);
|
|
23
|
+
|
|
24
|
+
if (segments.length === 0) return "Output";
|
|
25
|
+
|
|
26
|
+
return segments
|
|
27
|
+
.map((segment, i) => (i === 0 ? segment : segment.charAt(0).toUpperCase() + segment.slice(1)))
|
|
28
|
+
.join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
4
31
|
/**
|
|
5
32
|
* Represents a cross-lexicon output that bridges a producing lexicon (e.g. AWS)
|
|
6
33
|
* with any consuming lexicon (e.g. GitHub, Cloudflare).
|
|
@@ -76,12 +103,17 @@ export class LexiconOutput implements Intrinsic {
|
|
|
76
103
|
* Create a LexiconOutput with an auto-generated name from entity name and attribute.
|
|
77
104
|
* Used during cross-lexicon ref auto-detection.
|
|
78
105
|
*
|
|
106
|
+
* The name is sanitized to a valid CloudFormation logical id (see
|
|
107
|
+
* {@link sanitizeLogicalId}) since it is used verbatim as the `Outputs`
|
|
108
|
+
* key by lexicon serializers (e.g. AWS CloudFormation), which reject
|
|
109
|
+
* dots or underscores in logical ids.
|
|
110
|
+
*
|
|
79
111
|
* @param ref - The AttrRef pointing to the source entity
|
|
80
112
|
* @param entityName - The logical name of the source entity
|
|
81
|
-
* @returns A LexiconOutput with name `{entityName}
|
|
113
|
+
* @returns A LexiconOutput with a name derived from `{entityName}{Attribute}`
|
|
82
114
|
*/
|
|
83
115
|
static auto(ref: AttrRef, entityName: string): LexiconOutput {
|
|
84
|
-
const name =
|
|
116
|
+
const name = sanitizeLogicalId(entityName, ref.attribute);
|
|
85
117
|
const output = new LexiconOutput(ref, name);
|
|
86
118
|
output._setSourceEntity(entityName);
|
|
87
119
|
return output;
|