@intentius/chant-lexicon-azure 0.124.32 → 0.124.33

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.
Files changed (40) hide show
  1. package/dist/codegen/generate-typescript.js +1 -1
  2. package/dist/codegen/generate.js +1 -1
  3. package/dist/generated/index.d.ts.map +1 -1
  4. package/dist/generated/index.js +1 -1
  5. package/dist/health.d.ts +42 -0
  6. package/dist/health.d.ts.map +1 -0
  7. package/dist/health.js +137 -0
  8. package/dist/integrity.json +5 -5
  9. package/dist/lint/post-synth/arm-refs.d.ts +1 -0
  10. package/dist/lint/post-synth/arm-refs.d.ts.map +1 -1
  11. package/dist/lint/post-synth/arm-refs.js +5 -3
  12. package/dist/manifest.json +1 -1
  13. package/dist/meta.json +6 -1
  14. package/dist/okf/types/containerApps.md +5 -0
  15. package/dist/plugin.d.ts.map +1 -1
  16. package/dist/plugin.js +6 -0
  17. package/dist/rules/arm-refs.ts +5 -3
  18. package/dist/serializer.d.ts +7 -0
  19. package/dist/serializer.d.ts.map +1 -1
  20. package/dist/serializer.js +115 -29
  21. package/dist/spec/parse.d.ts.map +1 -1
  22. package/dist/spec/parse.js +10 -0
  23. package/dist/types/index.d.ts +5 -0
  24. package/package.json +2 -2
  25. package/src/attributes.test.ts +17 -0
  26. package/src/codegen/generate-typescript.ts +1 -1
  27. package/src/codegen/generate.ts +1 -1
  28. package/src/generated/index.d.ts +5 -0
  29. package/src/generated/index.ts +1 -1
  30. package/src/generated/lexicon-azure.json +6 -1
  31. package/src/health.test.ts +66 -0
  32. package/src/health.ts +162 -0
  33. package/src/lint/post-synth/arm-refs.ts +5 -3
  34. package/src/lint/post-synth/post-synth.test.ts +1 -1
  35. package/src/plugin.ts +8 -0
  36. package/src/serializer-names.test.ts +83 -0
  37. package/src/serializer-outputs.test.ts +65 -0
  38. package/src/serializer.ts +110 -28
  39. package/src/spec/parse.test.ts +15 -0
  40. package/src/spec/parse.ts +10 -0
package/src/serializer.ts CHANGED
@@ -170,20 +170,52 @@ export function getApiVersion(resourceType: string): string {
170
170
  return "2023-01-01";
171
171
  }
172
172
 
173
+ /**
174
+ * The ARM name of a chant export, as the arguments an ARM function takes
175
+ * (#4183). A resource's `name` prop is its ARM name, and the export name is
176
+ * used only when it has none. A literal name is quoted, one argument per
177
+ * segment of a child's `parent/child` name; a bracket expression such as
178
+ * `[parameters('n')]` is passed unwrapped.
179
+ */
180
+ function armNameArgs(entity: Declarable | undefined, exportName: string): { args: string; literal: string | null } {
181
+ const props = entity && isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null
182
+ ? entity.props as Record<string, unknown>
183
+ : undefined;
184
+ const name = typeof props?.name === "string" ? props.name : exportName;
185
+ if (name.startsWith("[") && name.endsWith("]")) return { args: name.slice(1, -1), literal: null };
186
+ return { args: name.split("/").map((seg) => `'${seg}'`).join(", "), literal: name };
187
+ }
188
+
189
+ function entityNamed(entityNames: Map<Declarable, string>, name: string): Declarable | undefined {
190
+ return [...entityNames.entries()].find(([, n]) => n === name)?.[0];
191
+ }
192
+
193
+ /** `resourceId(...)` for an export: the type and its ARM name. */
194
+ function armResourceId(entityNames: Map<Declarable, string>, name: string): string {
195
+ const entity = entityNamed(entityNames, name);
196
+ const { args } = armNameArgs(entity, name);
197
+ return entity ? `resourceId('${entity.entityType}', ${args})` : `resourceId(${args})`;
198
+ }
199
+
200
+ /**
201
+ * An attribute of an export. `id` is `resourceId()`, since `reference()`
202
+ * returns the resource's properties and they have no `id`; anything else
203
+ * reads `reference()` by the ARM name.
204
+ */
205
+ export function armAttrExpression(entityNames: Map<Declarable, string>, name: string, attr: string): string {
206
+ if (attr === "id") return `[${armResourceId(entityNames, name)}]`;
207
+ const { args, literal } = armNameArgs(entityNamed(entityNames, name), name);
208
+ const target = literal !== null && !literal.includes("/") ? args : armResourceId(entityNames, name);
209
+ return `[reference(${target}).${attr}]`;
210
+ }
211
+
173
212
  /**
174
213
  * ARM-specific visitor for the generic serializer walker.
175
214
  */
176
215
  function armVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
177
216
  return {
178
- attrRef: (name, attr) => `[reference('${name}').${attr}]`,
179
- resourceRef: (name) => {
180
- // Look up the resource type from entityNames
181
- const entity = [...entityNames.entries()].find(([, n]) => n === name)?.[0];
182
- if (entity) {
183
- return `[resourceId('${entity.entityType}', '${name}')]`;
184
- }
185
- return `[resourceId('${name}')]`;
186
- },
217
+ attrRef: (name, attr) => armAttrExpression(entityNames, name, attr),
218
+ resourceRef: (name) => `[${armResourceId(entityNames, name)}]`,
187
219
  propertyDeclarable: (entity, walk) => {
188
220
  if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
189
221
  return undefined;
@@ -203,6 +235,30 @@ function armVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
203
235
  /**
204
236
  * Convert a value to ARM-compatible JSON using the generic walker.
205
237
  */
238
+ /** An ARM output for a literal, or for an azure intrinsic's `[...]` expression. */
239
+ function armOutputValue(value: unknown): { type: string; value: unknown } | undefined {
240
+ if (typeof value === "string") return { type: "string", value };
241
+ if (typeof value === "number" && Number.isInteger(value)) return { type: "int", value };
242
+ if (typeof value === "boolean") return { type: "bool", value };
243
+ return undefined;
244
+ }
245
+
246
+ function isIntrinsicLike(value: unknown): value is { toJSON(): unknown } {
247
+ return typeof value === "object" && value !== null && typeof (value as { toJSON?: unknown }).toJSON === "function";
248
+ }
249
+
250
+ function isCloudFormationShaped(value: unknown): boolean {
251
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
252
+ const keys = Object.keys(value);
253
+ return keys.length === 1 && (keys[0] === "Ref" || keys[0].startsWith("Fn::"));
254
+ }
255
+
256
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
257
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
258
+ const proto = Object.getPrototypeOf(value);
259
+ return proto === Object.prototype || proto === null;
260
+ }
261
+
206
262
  function toArmValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
207
263
  return walkValue(value, entityNames, armVisitor(entityNames));
208
264
  }
@@ -214,6 +270,7 @@ function serializeToTemplate(
214
270
  entities: Map<string, Declarable>,
215
271
  outputs?: LexiconOutput[],
216
272
  ownership?: OwnershipMarker,
273
+ warnings: string[] = [],
217
274
  ): ArmTemplate {
218
275
  // Resolve the deployment scope from the resource types being emitted
219
276
  // (#1545): a template of tenant-only resources (management groups, policy
@@ -330,6 +387,12 @@ function serializeToTemplate(
330
387
  (resource as unknown as Record<string, unknown>)[key] = toArmValue(value, entityNames);
331
388
  } else if (key === "name") {
332
389
  resource.name = toArmValue(value, entityNames) as string;
390
+ } else if (key === "properties" && isPlainObject(value)) {
391
+ // The generated types take ARM's own `properties` object, so
392
+ // merge it, not nest it (#4183).
393
+ for (const [inner, innerValue] of Object.entries(value)) {
394
+ if (innerValue !== undefined) armProperties[inner] = toArmValue(innerValue, entityNames);
395
+ }
333
396
  } else {
334
397
  armProperties[key] = toArmValue(value, entityNames);
335
398
  }
@@ -356,14 +419,9 @@ function serializeToTemplate(
356
419
  if (attrs?.DependsOn !== undefined) {
357
420
  const resolved = resolveDependsOn(attrs.DependsOn, entityNames, name);
358
421
  if (resolved.length > 0) {
359
- resource.dependsOn = resolved.map((dep) => {
360
- // Convert to resourceId expression
361
- const depEntity = [...entityNames.entries()].find(([, n]) => n === dep)?.[0];
362
- if (depEntity) {
363
- return `[resourceId('${depEntity.entityType}', '${dep}')]`;
364
- }
365
- return dep;
366
- });
422
+ resource.dependsOn = resolved.map((dep) =>
423
+ entityNamed(entityNames, dep) ? `[${armResourceId(entityNames, dep)}]` : dep,
424
+ );
367
425
  }
368
426
  }
369
427
 
@@ -424,7 +482,7 @@ function serializeToTemplate(
424
482
  // Find the resource to build the resourceId expression
425
483
  const depResource = template.resources.find((r) => r.name === refName);
426
484
  if (depResource) {
427
- const depExpr = `[resourceId('${depResource.type}', '${refName}')]`;
485
+ const depExpr = `[resourceId('${depResource.type}', ${refName.split("/").map((seg) => `'${seg}'`).join(", ")})]`;
428
486
  if (!existing.has(depExpr)) {
429
487
  existing.add(depExpr);
430
488
  }
@@ -442,17 +500,20 @@ function serializeToTemplate(
442
500
  if (!template.outputs) template.outputs = {};
443
501
  const stackOutput = entity as StackOutput;
444
502
  // `unknown` so isAttrRefLike narrows cleanly across the workspace/published
445
- // type boundary. An intrinsic-wrapped output (#517) has no bare ARM
446
- // reference form, so skip it here (only aws/CFN emits those).
503
+ // type boundary.
447
504
  const ref: unknown = stackOutput.sourceRef;
448
505
  if (isAttrRefLike(ref)) {
449
506
  const logicalName = ref.getLogicalName();
450
507
  if (logicalName) {
451
508
  template.outputs[name] = {
452
509
  type: "string",
453
- value: `[reference('${logicalName}').${ref.attribute}]`,
510
+ value: armAttrExpression(entityNames, logicalName, ref.attribute),
454
511
  };
455
512
  }
513
+ } else {
514
+ const value = armOutputValue(isIntrinsicLike(ref) ? ref.toJSON() : ref);
515
+ if (value) template.outputs[name] = value;
516
+ else warnings.push(`azure: stack output "${name}" is not written: its value has no ARM form.`);
456
517
  }
457
518
  }
458
519
  }
@@ -461,11 +522,27 @@ function serializeToTemplate(
461
522
  if (outputs && outputs.length > 0) {
462
523
  template.outputs = template.outputs ?? {};
463
524
  for (const output of outputs) {
464
- template.outputs[output.outputName] = {
465
- type: "string",
466
- value: `[reference('${output.sourceEntity}').${output.sourceAttribute}]`,
467
- };
525
+ if (output.sourceAttribute !== null) {
526
+ if (output.sourceEntity) {
527
+ template.outputs[output.outputName] = {
528
+ type: "string",
529
+ value: armAttrExpression(entityNames, output.sourceEntity, output.sourceAttribute),
530
+ };
531
+ } else {
532
+ warnings.push(`azure: output "${output.outputName}" is not written: its resource has no name in this build.`);
533
+ }
534
+ continue;
535
+ }
536
+ // A literal or an intrinsic (#4181). It has no source lexicon, so every
537
+ // lexicon receives it; a CloudFormation-shaped value is aws's to write.
538
+ const raw = output.getOutputValue();
539
+ const value = armOutputValue(raw);
540
+ if (value) template.outputs[output.outputName] = value;
541
+ else if (!isCloudFormationShaped(raw)) {
542
+ warnings.push(`azure: output "${output.outputName}" is not written: its value has no ARM form.`);
543
+ }
468
544
  }
545
+ if (Object.keys(template.outputs).length === 0) delete template.outputs;
469
546
  }
470
547
 
471
548
  return template;
@@ -519,13 +596,18 @@ export const azureSerializer: Serializer = {
519
596
  }
520
597
  }
521
598
 
522
- const template = serializeToTemplate(entities, outputs, context?.ownership);
599
+ const warnings: string[] = [];
600
+ const template = serializeToTemplate(entities, outputs, context?.ownership, warnings);
523
601
  const primary = JSON.stringify(template, null, 2);
524
602
 
525
- if (!hasChildProjects) {
603
+ if (!hasChildProjects && warnings.length === 0) {
526
604
  return primary;
527
605
  }
528
606
 
529
- return { primary, files: allFiles };
607
+ return {
608
+ primary,
609
+ ...(hasChildProjects ? { files: allFiles } : {}),
610
+ ...(warnings.length > 0 ? { warnings } : {}),
611
+ };
530
612
  },
531
613
  };
@@ -68,6 +68,21 @@ describe("parseArmSchema", () => {
68
68
  expect(result.resource.attributes.find((a) => a.name === "id")).toBeTruthy();
69
69
  });
70
70
 
71
+ it("gives a Container App its hostname attributes (#4179)", () => {
72
+ const schema = {
73
+ resourceType: "Microsoft.App/containerApps",
74
+ apiVersion: "2024-03-01",
75
+ provider: "Microsoft.App",
76
+ resourceName: "containerApps",
77
+ resourceDefinition: { properties: {} },
78
+ definitions: {},
79
+ };
80
+
81
+ const names = parseArmSchema(JSON.stringify(schema)).resource.attributes.map((a) => a.name);
82
+ expect(names).toContain("configuration.ingress.fqdn");
83
+ expect(names).toContain("latestRevisionFqdn");
84
+ });
85
+
71
86
  it("parses definitions into property types and enums", () => {
72
87
  const schema = {
73
88
  resourceType: "Microsoft.Test/resources",
package/src/spec/parse.ts CHANGED
@@ -168,6 +168,16 @@ const CURATED_ATTRIBUTES: Record<string, Array<{ name: string; tsType: string }>
168
168
  { name: "id", tsType: "string" },
169
169
  { name: "provisioningState", tsType: "string" },
170
170
  ],
171
+ // The app's hostname, for DNS in front of it (#4179). Dotted names read a
172
+ // nested field of reference()'s properties, as AWS's Endpoint.Address does.
173
+ "Microsoft.App/containerApps": [
174
+ { name: "id", tsType: "string" },
175
+ { name: "configuration.ingress.fqdn", tsType: "string" },
176
+ { name: "latestRevisionFqdn", tsType: "string" },
177
+ { name: "latestRevisionName", tsType: "string" },
178
+ { name: "outboundIpAddresses", tsType: "string[]" },
179
+ { name: "provisioningState", tsType: "string" },
180
+ ],
171
181
  };
172
182
 
173
183
  /**