@intentius/chant-lexicon-azure 0.124.30 → 0.124.32

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 (117) hide show
  1. package/dist/actions/appservice.js +7 -0
  2. package/dist/actions/compute.js +9 -0
  3. package/dist/actions/container.js +14 -0
  4. package/dist/actions/identity.js +11 -0
  5. package/dist/actions/index.js +11 -0
  6. package/dist/actions/keyvault.js +12 -0
  7. package/dist/actions/network.js +9 -0
  8. package/dist/actions/sql.js +9 -0
  9. package/dist/actions/storage.js +15 -0
  10. package/dist/api/read-client.js +146 -0
  11. package/dist/codegen/docs-cli.js +2 -0
  12. package/dist/codegen/docs.js +83 -0
  13. package/dist/codegen/generate-cli.js +11 -0
  14. package/dist/codegen/generate-lexicon.js +77 -0
  15. package/dist/codegen/generate-typescript.js +163 -0
  16. package/dist/codegen/generate.js +106 -0
  17. package/dist/codegen/naming.js +139 -0
  18. package/dist/codegen/package.js +64 -0
  19. package/dist/composites/aks-cluster.js +68 -0
  20. package/dist/composites/app-service.js +62 -0
  21. package/dist/composites/application-gateway.js +107 -0
  22. package/dist/composites/catalog.js +856 -0
  23. package/dist/composites/container-instance.js +36 -0
  24. package/dist/composites/container-registry.js +62 -0
  25. package/dist/composites/cosmos-database.js +56 -0
  26. package/dist/composites/from-arm.js +47 -0
  27. package/dist/composites/function-app.js +57 -0
  28. package/dist/composites/index.js +16 -0
  29. package/dist/composites/keyvault.js +69 -0
  30. package/dist/composites/landing-zone.js +119 -0
  31. package/dist/composites/private-endpoint.js +61 -0
  32. package/dist/composites/redis-cache.js +21 -0
  33. package/dist/composites/service-bus-pipeline.js +54 -0
  34. package/dist/composites/sql-database.js +62 -0
  35. package/dist/composites/storage-account.js +59 -0
  36. package/dist/composites/vm-linux.js +139 -0
  37. package/dist/composites/vnet-default.js +102 -0
  38. package/dist/coverage.js +28 -0
  39. package/dist/deep-observe.js +356 -0
  40. package/dist/default-tags.js +50 -0
  41. package/dist/deploy-scopes.d.ts.map +1 -1
  42. package/dist/deploy-scopes.js +157 -0
  43. package/dist/describe-resources.js +187 -0
  44. package/dist/detect.js +17 -0
  45. package/dist/export-resources.js +108 -0
  46. package/dist/generated/index.js +16598 -0
  47. package/dist/generated/runtime.js +4 -0
  48. package/dist/governance.js +144 -0
  49. package/dist/import/generator.js +140 -0
  50. package/dist/import/live-export.js +39 -0
  51. package/dist/import/parser.js +192 -0
  52. package/dist/index.js +44 -0
  53. package/dist/integrity.json +2 -2
  54. package/dist/intrinsics.js +179 -0
  55. package/dist/lint/audit-catalog.js +36 -0
  56. package/dist/lint/audit-lineage.js +71 -0
  57. package/dist/lint/post-synth/arm-refs.js +94 -0
  58. package/dist/lint/post-synth/azr010.js +45 -0
  59. package/dist/lint/post-synth/azr011.js +43 -0
  60. package/dist/lint/post-synth/azr012.js +41 -0
  61. package/dist/lint/post-synth/azr013.js +50 -0
  62. package/dist/lint/post-synth/azr014.js +49 -0
  63. package/dist/lint/post-synth/azr015.js +66 -0
  64. package/dist/lint/post-synth/azr016.js +37 -0
  65. package/dist/lint/post-synth/azr017.js +37 -0
  66. package/dist/lint/post-synth/azr018.js +42 -0
  67. package/dist/lint/post-synth/azr019.js +41 -0
  68. package/dist/lint/post-synth/azr020.js +37 -0
  69. package/dist/lint/post-synth/azr021.js +36 -0
  70. package/dist/lint/post-synth/azr022.js +39 -0
  71. package/dist/lint/post-synth/azr023.js +39 -0
  72. package/dist/lint/post-synth/azr024.js +39 -0
  73. package/dist/lint/post-synth/azr025.js +37 -0
  74. package/dist/lint/post-synth/azr026.js +38 -0
  75. package/dist/lint/post-synth/azr027.js +37 -0
  76. package/dist/lint/post-synth/azr028.js +37 -0
  77. package/dist/lint/post-synth/azr029.js +38 -0
  78. package/dist/lint/post-synth/azr030.js +44 -0
  79. package/dist/lint/post-synth/azr031.js +36 -0
  80. package/dist/lint/post-synth/azr032.js +53 -0
  81. package/dist/lint/post-synth/azr033.js +35 -0
  82. package/dist/lint/post-synth/index.js +50 -0
  83. package/dist/lint/rules/hardcoded-location.js +82 -0
  84. package/dist/lint/rules/index.js +6 -0
  85. package/dist/lint/rules/nsg-wildcard.js +44 -0
  86. package/dist/lint/rules/storage-https.js +62 -0
  87. package/dist/lsp/completions.d.ts.map +1 -1
  88. package/dist/lsp/completions.js +18 -0
  89. package/dist/lsp/hover.d.ts.map +1 -1
  90. package/dist/lsp/hover.js +44 -0
  91. package/dist/manifest.json +1 -1
  92. package/dist/op/activities/az-apply.js +563 -0
  93. package/dist/op/activities/azure.js +48 -0
  94. package/dist/op/activities/floci-az.js +37 -0
  95. package/dist/op/activities/index.js +10 -0
  96. package/dist/op/builders.js +70 -0
  97. package/dist/ownership.js +13 -0
  98. package/dist/package-cli.js +21 -0
  99. package/dist/parameter.js +13 -0
  100. package/dist/plugin.d.ts.map +1 -1
  101. package/dist/plugin.js +472 -0
  102. package/dist/pseudo.js +40 -0
  103. package/dist/serializer.js +457 -0
  104. package/dist/spec/api-versions.js +130 -0
  105. package/dist/spec/fetch.js +196 -0
  106. package/dist/spec/parse.js +308 -0
  107. package/dist/taggable.js +34 -0
  108. package/dist/validate-cli.js +16 -0
  109. package/dist/validate.js +53 -0
  110. package/package.json +8 -8
  111. package/src/codegen/package.ts +2 -2
  112. package/src/deploy-scopes.ts +2 -1
  113. package/src/lsp/completions.ts +2 -1
  114. package/src/lsp/hover.ts +2 -1
  115. package/src/plugin.ts +3 -2
  116. package/src/serializer.ts +2 -2
  117. package/src/taggable.ts +2 -2
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Azure Resource Manager template serializer.
3
+ *
4
+ * Converts Declarable entities into ARM template JSON with:
5
+ * - resources[] array (not keyed by logical name like CloudFormation)
6
+ * - apiVersion per resource (from lexicon registry)
7
+ * - Resource-level fields (sku, kind, identity, tags, zones, plan, location)
8
+ * hoisted from properties
9
+ * - ARM bracket expression references
10
+ * - parameters, outputs sections
11
+ */
12
+ import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
13
+ import { ownershipEntries } from "@intentius/chant/ownership";
14
+ import { AZURE_TAG_OWNERSHIP_KEYS } from "./ownership.js";
15
+ import { walkValue } from "@intentius/chant/serializer-walker";
16
+ import { isChildProject } from "@intentius/chant/child-project";
17
+ import { existsSync, readFileSync } from "fs";
18
+ import { join } from "path";
19
+ import { moduleSourceDir } from "@intentius/chant/module-source";
20
+ import { isStackOutput } from "@intentius/chant/stack-output";
21
+ import { isAttrRefLike } from "@intentius/chant/utils";
22
+ import { resolveDependsOn } from "@intentius/chant/resource-attributes";
23
+ import { isDefaultTags } from "./default-tags.js";
24
+ import { loadTaggableResources } from "./taggable.js";
25
+ import { findArmResourceRefs } from "./lint/post-synth/arm-refs.js";
26
+ import { isDeploymentScope, resolveTemplateScope, TEMPLATE_SCHEMAS } from "./deploy-scopes.js";
27
+ /** Check if a declarable is a CoreParameter */
28
+ function isCoreParameter(entity) {
29
+ return "parameterType" in entity;
30
+ }
31
+ /** Resource-level fields that get hoisted from properties to resource level. */
32
+ const RESOURCE_LEVEL_FIELDS = new Set([
33
+ "location", "tags", "sku", "kind", "identity", "zones", "plan",
34
+ ]);
35
+ /**
36
+ * Load apiVersion for a resource type from the lexicon registry.
37
+ *
38
+ * Reads the generated registry (src/generated/lexicon-azure.json in a dev
39
+ * checkout, dist/meta.json in the installed package). Resolves the package
40
+ * directory from import.meta.url so it works under pure ESM as well as
41
+ * tsx/vitest — the old `require()`-based resolver threw under ESM and the
42
+ * throw was swallowed, so every resource emitted the fallback apiVersion
43
+ * (#1581). A missing registry is now an error: a uniform fallback apiVersion
44
+ * silently bypasses the per-provider pins in spec/api-versions.ts.
45
+ */
46
+ function resolveDir() {
47
+ return moduleSourceDir(import.meta.url);
48
+ }
49
+ export function apiVersionRegistryCandidates(dir = resolveDir()) {
50
+ return [
51
+ join(dir, "generated", "lexicon-azure.json"),
52
+ join(dir, "..", "dist", "meta.json"),
53
+ ];
54
+ }
55
+ export function loadApiVersions(candidates = apiVersionRegistryCandidates()) {
56
+ const map = new Map();
57
+ let content;
58
+ for (const candidate of candidates) {
59
+ if (!existsSync(candidate))
60
+ continue;
61
+ content = readFileSync(candidate, "utf-8");
62
+ break;
63
+ }
64
+ if (content === undefined) {
65
+ throw new Error(`azure: apiVersion registry not found (tried ${candidates.join(", ")}). ` +
66
+ "Run `npm run generate` (dev) or reinstall the package (dist/meta.json).");
67
+ }
68
+ const data = JSON.parse(content);
69
+ for (const entry of Object.values(data)) {
70
+ if (entry.apiVersion && entry.resourceType) {
71
+ map.set(entry.resourceType, entry.apiVersion);
72
+ }
73
+ }
74
+ if (map.size === 0) {
75
+ throw new Error("azure: apiVersion registry is empty — regenerate with `npm run generate`");
76
+ }
77
+ return map;
78
+ }
79
+ /**
80
+ * Override stale apiVersions from the generated registry.
81
+ * These are resource types where the registry apiVersion is known to be
82
+ * too old for modern ARM features (e.g. DataActions on role assignments).
83
+ */
84
+ const API_VERSION_OVERRIDES = new Map([
85
+ ["Microsoft.Authorization/roleAssignments", "2022-04-01"],
86
+ ["Microsoft.Authorization/roleDefinitions", "2022-04-01"],
87
+ ]);
88
+ let _apiVersions;
89
+ /** Test hook: drop the cached registry so the next lookup reloads it. */
90
+ export function resetApiVersionCache() {
91
+ _apiVersions = undefined;
92
+ }
93
+ /**
94
+ * Registry lookup without the serializer's fallback: the pinned apiVersion for
95
+ * `resourceType`, or `undefined` when neither the overrides nor the generated
96
+ * registry know the type. ARM resource types are case-insensitive and the live
97
+ * listing does not always echo the casing the registry uses, so the match is
98
+ * case-insensitive too. Used by prune (#1472), which needs an apiVersion for a
99
+ * type the current template no longer declares.
100
+ */
101
+ export function lookupApiVersion(resourceType) {
102
+ const override = API_VERSION_OVERRIDES.get(resourceType);
103
+ if (override)
104
+ return override;
105
+ if (!_apiVersions)
106
+ _apiVersions = loadApiVersions();
107
+ const exact = _apiVersions.get(resourceType);
108
+ if (exact)
109
+ return exact;
110
+ const wanted = resourceType.toLowerCase();
111
+ for (const [type, version] of _apiVersions) {
112
+ if (type.toLowerCase() === wanted)
113
+ return version;
114
+ }
115
+ return undefined;
116
+ }
117
+ export function getApiVersion(resourceType) {
118
+ const version = lookupApiVersion(resourceType);
119
+ if (version)
120
+ return version;
121
+ console.warn(`azure: no pinned apiVersion for ${resourceType} in the lexicon registry; falling back to 2023-01-01`);
122
+ return "2023-01-01";
123
+ }
124
+ /**
125
+ * ARM-specific visitor for the generic serializer walker.
126
+ */
127
+ function armVisitor(entityNames) {
128
+ return {
129
+ attrRef: (name, attr) => `[reference('${name}').${attr}]`,
130
+ resourceRef: (name) => {
131
+ // Look up the resource type from entityNames
132
+ const entity = [...entityNames.entries()].find(([, n]) => n === name)?.[0];
133
+ if (entity) {
134
+ return `[resourceId('${entity.entityType}', '${name}')]`;
135
+ }
136
+ return `[resourceId('${name}')]`;
137
+ },
138
+ propertyDeclarable: (entity, walk) => {
139
+ if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
140
+ return undefined;
141
+ }
142
+ const props = entity.props;
143
+ const armProps = {};
144
+ for (const [key, value] of Object.entries(props)) {
145
+ if (value !== undefined) {
146
+ armProps[key] = walk(value);
147
+ }
148
+ }
149
+ return Object.keys(armProps).length > 0 ? armProps : undefined;
150
+ },
151
+ };
152
+ }
153
+ /**
154
+ * Convert a value to ARM-compatible JSON using the generic walker.
155
+ */
156
+ function toArmValue(value, entityNames) {
157
+ return walkValue(value, entityNames, armVisitor(entityNames));
158
+ }
159
+ /**
160
+ * Serialize entities into an ARM template object.
161
+ */
162
+ function serializeToTemplate(entities, outputs, ownership) {
163
+ // Resolve the deployment scope from the resource types being emitted
164
+ // (#1545): a template of tenant-only resources (management groups, policy
165
+ // definitions at management-group scope, ...) must carry the matching
166
+ // $schema and must not lean on resourceGroup() expressions. Mixed sets
167
+ // with no common scope fall back to resource-group scope and AZR030
168
+ // reports the resources that cannot deploy there. A deploymentScope()
169
+ // pin overrides the inference — policy definitions and assignments
170
+ // deploy at several scopes, so a management-group guardrail project
171
+ // cannot be told from a subscription one by its resource set alone
172
+ //; AZR030 still flags resources the pinned scope cannot hold.
173
+ let pinnedScope;
174
+ const emittedTypes = [];
175
+ for (const [, entity] of entities) {
176
+ if (isStackOutput(entity) || isDefaultTags(entity) || isCoreParameter(entity))
177
+ continue;
178
+ if (isDeploymentScope(entity)) {
179
+ pinnedScope ?? (pinnedScope = entity.scope);
180
+ }
181
+ else if (isChildProject(entity)) {
182
+ emittedTypes.push("Microsoft.Resources/deployments");
183
+ }
184
+ else if (!isPropertyDeclarable(entity)) {
185
+ emittedTypes.push(entity.entityType);
186
+ }
187
+ }
188
+ const templateScope = pinnedScope ?? resolveTemplateScope(emittedTypes);
189
+ const template = {
190
+ $schema: TEMPLATE_SCHEMAS[templateScope],
191
+ contentVersion: "1.0.0.0",
192
+ resources: [],
193
+ };
194
+ // Build reverse map: entity -> name
195
+ const entityNames = new Map();
196
+ for (const [name, entity] of entities) {
197
+ entityNames.set(entity, name);
198
+ }
199
+ // Collect default tags. The ownership marker is stamped as tags, seeded
200
+ // first so user default tags (and explicit per-resource tags) win.
201
+ const defaultTagEntries = [];
202
+ if (ownership) {
203
+ for (const [key, value] of Object.entries(ownershipEntries(AZURE_TAG_OWNERSHIP_KEYS, ownership))) {
204
+ defaultTagEntries.push({ key, value });
205
+ }
206
+ }
207
+ for (const [, entity] of entities) {
208
+ if (isDefaultTags(entity)) {
209
+ defaultTagEntries.push(...entity.tags);
210
+ }
211
+ }
212
+ // Process entities
213
+ for (const [name, entity] of entities) {
214
+ // Skip non-resource types
215
+ if (isStackOutput(entity) || isDefaultTags(entity) || isDeploymentScope(entity))
216
+ continue;
217
+ if (isCoreParameter(entity)) {
218
+ if (!template.parameters)
219
+ template.parameters = {};
220
+ const param = {
221
+ type: mapParameterType(entity.parameterType),
222
+ };
223
+ if ("description" in entity && typeof entity.description === "string") {
224
+ param.metadata = { description: entity.description };
225
+ }
226
+ if ("defaultValue" in entity && entity.defaultValue !== undefined) {
227
+ param.defaultValue = entity.defaultValue;
228
+ }
229
+ template.parameters[name] = param;
230
+ }
231
+ else if (isChildProject(entity)) {
232
+ // ChildProjectInstance → linked deployment
233
+ const childProject = entity;
234
+ const childName = childProject.logicalName;
235
+ const filename = `${childName}.template.json`;
236
+ const resource = {
237
+ type: "Microsoft.Resources/deployments",
238
+ apiVersion: "2021-04-01",
239
+ name: childName,
240
+ properties: {
241
+ mode: "Incremental",
242
+ templateLink: { uri: filename },
243
+ },
244
+ };
245
+ template.resources.push(resource);
246
+ }
247
+ else if (!isPropertyDeclarable(entity)) {
248
+ const resourceType = entity.entityType;
249
+ // Prefer apiVersion from entity attributes (set by composites) over registry lookup
250
+ const attrs0 = (isResourceDeclarable(entity) && typeof entity.attributes === "object" && entity.attributes !== null)
251
+ ? entity.attributes
252
+ : undefined;
253
+ const apiVersion = (typeof attrs0?.apiVersion === "string") ? attrs0.apiVersion : getApiVersion(resourceType);
254
+ const resource = {
255
+ type: resourceType,
256
+ apiVersion,
257
+ name: name,
258
+ };
259
+ // Extract all props
260
+ if (isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null) {
261
+ const props = entity.props;
262
+ const armProperties = {};
263
+ for (const [key, value] of Object.entries(props)) {
264
+ if (value === undefined)
265
+ continue;
266
+ if (RESOURCE_LEVEL_FIELDS.has(key)) {
267
+ // Hoist to resource level
268
+ resource[key] = toArmValue(value, entityNames);
269
+ }
270
+ else if (key === "name") {
271
+ resource.name = toArmValue(value, entityNames);
272
+ }
273
+ else {
274
+ armProperties[key] = toArmValue(value, entityNames);
275
+ }
276
+ }
277
+ if (Object.keys(armProperties).length > 0) {
278
+ resource.properties = armProperties;
279
+ }
280
+ }
281
+ // Default location to resource group location if not specified.
282
+ // Above resource-group scope there is no resource group to read a
283
+ // location from, and the scoped resources (management groups, policy
284
+ // definitions, ...) have no location field at all (#1545).
285
+ if (!resource.location && templateScope === "resourceGroup") {
286
+ resource.location = "[resourceGroup().location]";
287
+ }
288
+ // Handle DependsOn
289
+ const attrs = (isResourceDeclarable(entity) && typeof entity.attributes === "object" && entity.attributes !== null)
290
+ ? entity.attributes
291
+ : undefined;
292
+ if (attrs?.DependsOn !== undefined) {
293
+ const resolved = resolveDependsOn(attrs.DependsOn, entityNames, name);
294
+ if (resolved.length > 0) {
295
+ resource.dependsOn = resolved.map((dep) => {
296
+ // Convert to resourceId expression
297
+ const depEntity = [...entityNames.entries()].find(([, n]) => n === dep)?.[0];
298
+ if (depEntity) {
299
+ return `[resourceId('${depEntity.entityType}', '${dep}')]`;
300
+ }
301
+ return dep;
302
+ });
303
+ }
304
+ }
305
+ template.resources.push(resource);
306
+ }
307
+ }
308
+ // Inject default tags into taggable resources
309
+ if (defaultTagEntries.length > 0) {
310
+ const taggable = loadTaggableResources();
311
+ for (const resource of template.resources) {
312
+ if (!taggable.has(resource.type))
313
+ continue;
314
+ const resolved = {};
315
+ for (const t of defaultTagEntries) {
316
+ resolved[t.key] = toArmValue(t.value, entityNames);
317
+ }
318
+ const existing = resource.tags ?? {};
319
+ // Explicit tags take precedence
320
+ resource.tags = { ...resolved, ...existing };
321
+ }
322
+ }
323
+ // Auto-infer dependsOn from resourceId()/reference() expressions in properties.
324
+ // ARM only auto-infers dependencies from reference(), not resourceId().
325
+ const resourceNameSet = new Set(template.resources.map((r) => r.name));
326
+ for (const resource of template.resources) {
327
+ const refs = findArmResourceRefs(resource.properties);
328
+ // Also scan resource-level fields (location, tags, identity, etc.)
329
+ for (const field of RESOURCE_LEVEL_FIELDS) {
330
+ const val = resource[field];
331
+ if (val !== undefined) {
332
+ for (const ref of findArmResourceRefs(val)) {
333
+ refs.add(ref);
334
+ }
335
+ }
336
+ }
337
+ // Also scan the name field for parent references (e.g. "vnet/subnet")
338
+ if (typeof resource.name === "string") {
339
+ for (const ref of findArmResourceRefs(resource.name)) {
340
+ refs.add(ref);
341
+ }
342
+ }
343
+ // Infer parent dependency for child resources (e.g. "vnet/subnet" depends on "vnet")
344
+ if (typeof resource.name === "string" && resource.name.includes("/")) {
345
+ const parentName = resource.name.split("/")[0];
346
+ if (resourceNameSet.has(parentName)) {
347
+ refs.add(parentName);
348
+ }
349
+ }
350
+ if (refs.size > 0) {
351
+ const existing = new Set(resource.dependsOn ?? []);
352
+ for (const refName of refs) {
353
+ // Only add if the referenced resource exists in this template and isn't self
354
+ if (refName === resource.name)
355
+ continue;
356
+ if (!resourceNameSet.has(refName))
357
+ continue;
358
+ // Find the resource to build the resourceId expression
359
+ const depResource = template.resources.find((r) => r.name === refName);
360
+ if (depResource) {
361
+ const depExpr = `[resourceId('${depResource.type}', '${refName}')]`;
362
+ if (!existing.has(depExpr)) {
363
+ existing.add(depExpr);
364
+ }
365
+ }
366
+ }
367
+ if (existing.size > 0) {
368
+ resource.dependsOn = [...existing];
369
+ }
370
+ }
371
+ }
372
+ // Emit StackOutput entities as ARM outputs
373
+ for (const [name, entity] of entities) {
374
+ if (isStackOutput(entity)) {
375
+ if (!template.outputs)
376
+ template.outputs = {};
377
+ const stackOutput = entity;
378
+ // `unknown` so isAttrRefLike narrows cleanly across the workspace/published
379
+ // type boundary. An intrinsic-wrapped output (#517) has no bare ARM
380
+ // reference form, so skip it here (only aws/CFN emits those).
381
+ const ref = stackOutput.sourceRef;
382
+ if (isAttrRefLike(ref)) {
383
+ const logicalName = ref.getLogicalName();
384
+ if (logicalName) {
385
+ template.outputs[name] = {
386
+ type: "string",
387
+ value: `[reference('${logicalName}').${ref.attribute}]`,
388
+ };
389
+ }
390
+ }
391
+ }
392
+ }
393
+ // Add LexiconOutputs
394
+ if (outputs && outputs.length > 0) {
395
+ template.outputs = template.outputs ?? {};
396
+ for (const output of outputs) {
397
+ template.outputs[output.outputName] = {
398
+ type: "string",
399
+ value: `[reference('${output.sourceEntity}').${output.sourceAttribute}]`,
400
+ };
401
+ }
402
+ }
403
+ return template;
404
+ }
405
+ /**
406
+ * Map chant parameter types to ARM parameter types.
407
+ */
408
+ function mapParameterType(paramType) {
409
+ switch (paramType) {
410
+ case "String": return "string";
411
+ case "Number": return "int";
412
+ case "CommaDelimitedList": return "array";
413
+ default: return "string";
414
+ }
415
+ }
416
+ /**
417
+ * Azure ARM template serializer implementation.
418
+ */
419
+ export const azureSerializer = {
420
+ name: "azure",
421
+ rulePrefix: "AZR",
422
+ serialize(entities, outputs, context) {
423
+ // Check for child projects (linked templates)
424
+ let hasChildProjects = false;
425
+ const allFiles = {};
426
+ for (const [, entity] of entities) {
427
+ if (isChildProject(entity)) {
428
+ hasChildProjects = true;
429
+ const childProject = entity;
430
+ if (childProject.buildResult) {
431
+ const childOutput = childProject.buildResult.outputs.get("azure");
432
+ if (childOutput) {
433
+ const childName = childProject.logicalName;
434
+ const filename = `${childName}.template.json`;
435
+ if (typeof childOutput === "string") {
436
+ allFiles[filename] = childOutput;
437
+ }
438
+ else {
439
+ allFiles[filename] = childOutput.primary;
440
+ if (childOutput.files) {
441
+ for (const [f, c] of Object.entries(childOutput.files)) {
442
+ allFiles[f] = c;
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+ const template = serializeToTemplate(entities, outputs, context?.ownership);
451
+ const primary = JSON.stringify(template, null, 2);
452
+ if (!hasChildProjects) {
453
+ return primary;
454
+ }
455
+ return { primary, files: allFiles };
456
+ },
457
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * API version resolution for Azure Resource Manager schemas.
3
+ *
4
+ * ARM schemas are organized by API date:
5
+ * schemas/{date}/Microsoft.{Provider}.json
6
+ *
7
+ * This module picks the latest API version per provider.
8
+ */
9
+ /**
10
+ * Parse an API date string (e.g. "2023-01-01" or "2023-01-01-preview").
11
+ * Returns a numeric value for comparison: date timestamp with preview
12
+ * versions ranked below their GA counterpart.
13
+ */
14
+ export function parseApiDate(dateStr) {
15
+ const isPreview = dateStr.endsWith("-preview");
16
+ const cleaned = isPreview ? dateStr.replace(/-preview$/, "") : dateStr;
17
+ const ts = new Date(cleaned).getTime();
18
+ // Preview versions sort just below their GA date
19
+ return isPreview ? ts - 1 : ts;
20
+ }
21
+ /**
22
+ * Compare two API date strings. Returns positive if a > b.
23
+ */
24
+ export function compareApiDates(a, b) {
25
+ return parseApiDate(a) - parseApiDate(b);
26
+ }
27
+ /**
28
+ * Extract provider name and API version from a schema file path.
29
+ *
30
+ * Input: "azure-resource-manager-schemas-main/schemas/2023-01-01/Microsoft.Storage.json"
31
+ * Output: { provider: "Microsoft.Storage", apiVersion: "2023-01-01" }
32
+ */
33
+ export function parseSchemaPath(path) {
34
+ // Match schemas/{date}/Microsoft.{Provider}.json
35
+ const match = path.match(/schemas\/(\d{4}-\d{2}-\d{2}(?:-preview)?)\/(Microsoft\.[^/]+)\.json$/);
36
+ if (!match)
37
+ return null;
38
+ return { apiVersion: match[1], provider: match[2] };
39
+ }
40
+ /**
41
+ * Per-provider API-version pins.
42
+ *
43
+ * `latestVersionPerProvider` picks the single newest version per provider, but
44
+ * Azure spreads resources across versions — a newer version can DROP a resource
45
+ * an older one defined. Microsoft.Authorization's latest preview no longer
46
+ * includes the plain `roleAssignments` / `roleDefinitions` resources; they live
47
+ * in 2022-04-01, the latest stable that still has them. Pin it so those
48
+ * generate (the naming table maps both). See #223.
49
+ *
50
+ * Microsoft.Compute has the same issue (#1144): at the schema commit pinned
51
+ * in ../spec/fetch.ts, the newest dated `Microsoft.Compute.json` (2026-03-02)
52
+ * is a narrow disk-only delta that drops `virtualMachines`,
53
+ * `virtualMachineScaleSets`, and `availabilitySets` entirely — not a rename,
54
+ * an omission, since Azure alternates between a "VM flavor" and a "disk
55
+ * flavor" of this file across dates rather than publishing one cumulative
56
+ * file. 2026-03-01 is the most recent date that still has the VM family
57
+ * (validate.ts's REQUIRED_NAMES and composites/vm-linux.ts depend on it);
58
+ * it drops the disk-only resources (disks, diskAccesses, diskEncryptionSets,
59
+ * snapshots), which nothing here currently references.
60
+ *
61
+ * A pin can list several versions (#1545): Azure publishes some providers as
62
+ * per-family deltas, so no single date carries every resource we need. The
63
+ * files merge in the order listed — the first file that defines a resource
64
+ * name wins, later files only contribute resources not yet seen.
65
+ * Microsoft.Authorization needs three: 2022-04-01 for roleAssignments /
66
+ * roleDefinitions (#223), 2026-06-01 (latest stable policy file) for
67
+ * policyAssignments / policyDefinitions / policySetDefinitions and their
68
+ * _versions children, and 2022-07-01-preview for policyExemptions, which has
69
+ * no GA date and whose newer preview files (2024-12-01-preview and later)
70
+ * drag in policy variables we don't want yet.
71
+ *
72
+ * Microsoft.AzureStackHCI splits the same way (#1795): the newest dated file
73
+ * (2026-05-01-preview) carries only the cluster/edge-device family, while the
74
+ * VM family (virtualMachineInstances, virtualNetworks, networkInterfaces,
75
+ * publicIPAddresses, galleryImages, virtualHardDisks, ...) last appears in
76
+ * 2026-04-01-preview. Pin both, newest first — the cluster/edge family
77
+ * generates from 2026-05-01-preview and the VM family fills in from
78
+ * 2026-04-01-preview. `Microsoft.DataReplication/replicationVaults_
79
+ * privateEndpointConnectionProxies` dropped out in the same re-baseline but
80
+ * is NOT pinnable back: no dated Microsoft.DataReplication.json at the
81
+ * AZURE_SCHEMA_COMMIT pin (2021-02-16-preview, 2024-09-01, 2026-05-01)
82
+ * defines it — upstream removed the resource, so it stays gone.
83
+ *
84
+ * Microsoft.Management and Microsoft.Subscription are pinned to their latest
85
+ * GA dates: the naive latest for both is a preview (2024-02-01-preview adds
86
+ * serviceGroups; 2025-11-01-preview adds changeTenantRequest), and the
87
+ * management-group hierarchy should not author against preview apiVersions.
88
+ */
89
+ export const PROVIDER_VERSION_OVERRIDES = {
90
+ "Microsoft.Authorization": ["2022-04-01", "2026-06-01", "2022-07-01-preview"],
91
+ "Microsoft.AzureStackHCI": ["2026-05-01-preview", "2026-04-01-preview"],
92
+ "Microsoft.Compute": ["2026-03-01"],
93
+ "Microsoft.Management": ["2023-04-01"],
94
+ "Microsoft.Subscription": ["2021-10-01"],
95
+ };
96
+ /**
97
+ * Given a set of schema paths, return the schema files to generate from, per
98
+ * provider: the single latest API version, unless
99
+ * {@link PROVIDER_VERSION_OVERRIDES} pins the provider to one or more chosen
100
+ * versions (used where "latest" drops resources we depend on). For pinned
101
+ * providers the array preserves the pin order — callers merge the files
102
+ * first-wins per resource name.
103
+ *
104
+ * Returns a Map of provider → [{ path, apiVersion }, ...].
105
+ */
106
+ export function latestVersionPerProvider(paths) {
107
+ const best = new Map();
108
+ for (const p of paths) {
109
+ const parsed = parseSchemaPath(p);
110
+ if (!parsed)
111
+ continue;
112
+ const pinned = PROVIDER_VERSION_OVERRIDES[parsed.provider];
113
+ if (pinned) {
114
+ // Only accept the pinned versions for an overridden provider,
115
+ // slotted into pin order.
116
+ if (pinned.includes(parsed.apiVersion)) {
117
+ const files = best.get(parsed.provider) ?? [];
118
+ files.push({ path: p, apiVersion: parsed.apiVersion });
119
+ files.sort((a, b) => pinned.indexOf(a.apiVersion) - pinned.indexOf(b.apiVersion));
120
+ best.set(parsed.provider, files);
121
+ }
122
+ continue;
123
+ }
124
+ const existing = best.get(parsed.provider);
125
+ if (!existing || compareApiDates(parsed.apiVersion, existing[0].apiVersion) > 0) {
126
+ best.set(parsed.provider, [{ path: p, apiVersion: parsed.apiVersion }]);
127
+ }
128
+ }
129
+ return best;
130
+ }