@xpack/xpm-lib 3.1.2 → 4.0.0

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 (202) hide show
  1. package/README.md +16 -212
  2. package/dist/classes/actions.d.ts +58 -0
  3. package/dist/classes/actions.d.ts.map +1 -0
  4. package/dist/classes/actions.js +250 -0
  5. package/dist/classes/actions.js.map +1 -0
  6. package/dist/classes/build-configurations.d.ts +78 -0
  7. package/dist/classes/build-configurations.d.ts.map +1 -0
  8. package/dist/classes/build-configurations.js +489 -0
  9. package/dist/classes/build-configurations.js.map +1 -0
  10. package/dist/classes/combinations-generator.d.ts +19 -0
  11. package/dist/classes/combinations-generator.d.ts.map +1 -0
  12. package/dist/classes/combinations-generator.js +48 -0
  13. package/dist/classes/combinations-generator.js.map +1 -0
  14. package/dist/classes/data-model.d.ts +21 -0
  15. package/dist/classes/data-model.d.ts.map +1 -0
  16. package/dist/classes/data-model.js +47 -0
  17. package/dist/classes/data-model.js.map +1 -0
  18. package/dist/classes/errors.d.ts +13 -0
  19. package/dist/classes/errors.d.ts.map +1 -0
  20. package/dist/classes/errors.js +13 -0
  21. package/dist/classes/errors.js.map +1 -0
  22. package/dist/classes/init-template-base.d.ts +47 -0
  23. package/dist/classes/init-template-base.d.ts.map +1 -0
  24. package/dist/classes/init-template-base.js +358 -0
  25. package/dist/classes/init-template-base.js.map +1 -0
  26. package/dist/classes/liquid-drop.d.ts +28 -0
  27. package/dist/classes/liquid-drop.d.ts.map +1 -0
  28. package/dist/classes/liquid-drop.js +70 -0
  29. package/dist/classes/liquid-drop.js.map +1 -0
  30. package/dist/classes/liquid-engine.d.ts +7 -0
  31. package/dist/classes/liquid-engine.d.ts.map +1 -0
  32. package/dist/classes/liquid-engine.js +72 -0
  33. package/dist/classes/liquid-engine.js.map +1 -0
  34. package/dist/classes/package.d.ts +31 -0
  35. package/dist/classes/package.d.ts.map +1 -0
  36. package/dist/classes/package.js +268 -0
  37. package/dist/classes/package.js.map +1 -0
  38. package/dist/classes/platform-detector.d.ts +14 -0
  39. package/dist/classes/platform-detector.d.ts.map +1 -0
  40. package/dist/classes/platform-detector.js +26 -0
  41. package/dist/classes/platform-detector.js.map +1 -0
  42. package/dist/classes/policies.d.ts +14 -0
  43. package/dist/classes/policies.d.ts.map +1 -0
  44. package/dist/classes/policies.js +20 -0
  45. package/dist/classes/policies.js.map +1 -0
  46. package/dist/classes/template-expander.d.ts +29 -0
  47. package/dist/classes/template-expander.d.ts.map +1 -0
  48. package/dist/classes/template-expander.js +62 -0
  49. package/dist/classes/template-expander.js.map +1 -0
  50. package/dist/data/substitutions-variables.d.ts +43 -0
  51. package/dist/data/substitutions-variables.d.ts.map +1 -0
  52. package/dist/{lib → data}/substitutions-variables.js +1 -16
  53. package/dist/data/substitutions-variables.js.map +1 -0
  54. package/dist/functions/chmod-recursively.d.ts +9 -0
  55. package/dist/functions/chmod-recursively.d.ts.map +1 -0
  56. package/dist/functions/chmod-recursively.js +66 -0
  57. package/dist/functions/chmod-recursively.js.map +1 -0
  58. package/dist/functions/filter-paths.d.ts +5 -0
  59. package/dist/functions/filter-paths.d.ts.map +1 -0
  60. package/dist/functions/filter-paths.js +16 -0
  61. package/dist/functions/filter-paths.js.map +1 -0
  62. package/dist/functions/is-something.d.ts +9 -0
  63. package/dist/functions/is-something.d.ts.map +1 -0
  64. package/dist/functions/is-something.js +25 -0
  65. package/dist/functions/is-something.js.map +1 -0
  66. package/dist/functions/matrix-expander.d.ts +17 -0
  67. package/dist/functions/matrix-expander.d.ts.map +1 -0
  68. package/dist/functions/matrix-expander.js +52 -0
  69. package/dist/functions/matrix-expander.js.map +1 -0
  70. package/dist/functions/perform-substitutions.d.ts +12 -0
  71. package/dist/functions/perform-substitutions.d.ts.map +1 -0
  72. package/dist/functions/perform-substitutions.js +76 -0
  73. package/dist/functions/perform-substitutions.js.map +1 -0
  74. package/dist/functions/utils.d.ts +8 -0
  75. package/dist/functions/utils.d.ts.map +1 -0
  76. package/dist/functions/utils.js +16 -0
  77. package/dist/functions/utils.js.map +1 -0
  78. package/dist/index.d.ts +22 -15
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +22 -29
  81. package/dist/index.js.map +1 -1
  82. package/dist/{lib/types.d.ts → types/json.d.ts} +31 -22
  83. package/dist/types/json.d.ts.map +1 -0
  84. package/dist/types/json.js +2 -0
  85. package/dist/types/json.js.map +1 -0
  86. package/dist/types/xpm-init-template.d.ts +21 -0
  87. package/dist/types/xpm-init-template.d.ts.map +1 -0
  88. package/dist/types/xpm-init-template.js +2 -0
  89. package/dist/types/xpm-init-template.js.map +1 -0
  90. package/dist/types/xpm.d.ts +16 -0
  91. package/dist/types/xpm.d.ts.map +1 -0
  92. package/dist/types/xpm.js +2 -0
  93. package/dist/types/xpm.js.map +1 -0
  94. package/package.json +53 -44
  95. package/src/CODE-REVIEW.md +2167 -0
  96. package/src/README.md +393 -6
  97. package/src/classes/actions.ts +1157 -0
  98. package/src/classes/build-configurations.ts +2127 -0
  99. package/src/classes/combinations-generator.ts +331 -0
  100. package/src/classes/data-model.ts +337 -0
  101. package/src/classes/errors.ts +105 -0
  102. package/src/classes/init-template-base.ts +1028 -0
  103. package/src/classes/liquid-drop.ts +376 -0
  104. package/src/classes/liquid-engine.ts +249 -0
  105. package/src/classes/package.ts +765 -0
  106. package/src/classes/platform-detector.ts +237 -0
  107. package/src/classes/policies.ts +200 -0
  108. package/src/classes/template-expander.ts +330 -0
  109. package/src/data/substitutions-variables.ts +390 -0
  110. package/src/functions/chmod-recursively.ts +195 -0
  111. package/src/functions/filter-paths.ts +126 -0
  112. package/src/functions/is-something.ts +223 -0
  113. package/src/functions/matrix-expander.ts +172 -0
  114. package/src/functions/perform-substitutions.ts +253 -0
  115. package/src/functions/utils.ts +151 -0
  116. package/src/index.ts +72 -19
  117. package/src/types/json.ts +519 -0
  118. package/src/types/xpm-init-template.ts +282 -0
  119. package/src/types/xpm.ts +162 -0
  120. package/dist/lib/chmod-recursive.d.ts +0 -7
  121. package/dist/lib/chmod-recursive.d.ts.map +0 -1
  122. package/dist/lib/chmod-recursive.js +0 -81
  123. package/dist/lib/chmod-recursive.js.map +0 -1
  124. package/dist/lib/errors.d.ts +0 -11
  125. package/dist/lib/errors.d.ts.map +0 -1
  126. package/dist/lib/errors.js +0 -26
  127. package/dist/lib/errors.js.map +0 -1
  128. package/dist/lib/functions/chmod-recursive.d.ts +0 -7
  129. package/dist/lib/functions/chmod-recursive.d.ts.map +0 -1
  130. package/dist/lib/functions/chmod-recursive.js +0 -81
  131. package/dist/lib/functions/chmod-recursive.js.map +0 -1
  132. package/dist/lib/functions/perform-substitutions.d.ts +0 -20
  133. package/dist/lib/functions/perform-substitutions.d.ts.map +0 -1
  134. package/dist/lib/functions/perform-substitutions.js +0 -85
  135. package/dist/lib/functions/perform-substitutions.js.map +0 -1
  136. package/dist/lib/functions/utils.d.ts +0 -30
  137. package/dist/lib/functions/utils.d.ts.map +0 -1
  138. package/dist/lib/functions/utils.js +0 -70
  139. package/dist/lib/functions/utils.js.map +0 -1
  140. package/dist/lib/init-template-base.d.ts +0 -46
  141. package/dist/lib/init-template-base.d.ts.map +0 -1
  142. package/dist/lib/init-template-base.js +0 -281
  143. package/dist/lib/init-template-base.js.map +0 -1
  144. package/dist/lib/liquid-actions.d.ts +0 -37
  145. package/dist/lib/liquid-actions.d.ts.map +0 -1
  146. package/dist/lib/liquid-actions.js +0 -148
  147. package/dist/lib/liquid-actions.js.map +0 -1
  148. package/dist/lib/liquid-build-configurations.d.ts +0 -47
  149. package/dist/lib/liquid-build-configurations.d.ts.map +0 -1
  150. package/dist/lib/liquid-build-configurations.js +0 -282
  151. package/dist/lib/liquid-build-configurations.js.map +0 -1
  152. package/dist/lib/liquid-drop.d.ts +0 -13
  153. package/dist/lib/liquid-drop.d.ts.map +0 -1
  154. package/dist/lib/liquid-drop.js +0 -56
  155. package/dist/lib/liquid-drop.js.map +0 -1
  156. package/dist/lib/liquid-engine.d.ts +0 -5
  157. package/dist/lib/liquid-engine.d.ts.map +0 -1
  158. package/dist/lib/liquid-engine.js +0 -85
  159. package/dist/lib/liquid-engine.js.map +0 -1
  160. package/dist/lib/liquid-package.d.ts +0 -17
  161. package/dist/lib/liquid-package.d.ts.map +0 -1
  162. package/dist/lib/liquid-package.js +0 -70
  163. package/dist/lib/liquid-package.js.map +0 -1
  164. package/dist/lib/package.d.ts +0 -66
  165. package/dist/lib/package.d.ts.map +0 -1
  166. package/dist/lib/package.js +0 -700
  167. package/dist/lib/package.js.map +0 -1
  168. package/dist/lib/perform-substitutions.d.ts +0 -20
  169. package/dist/lib/perform-substitutions.d.ts.map +0 -1
  170. package/dist/lib/perform-substitutions.js +0 -85
  171. package/dist/lib/perform-substitutions.js.map +0 -1
  172. package/dist/lib/policies.d.ts +0 -14
  173. package/dist/lib/policies.d.ts.map +0 -1
  174. package/dist/lib/policies.js +0 -33
  175. package/dist/lib/policies.js.map +0 -1
  176. package/dist/lib/substitutions-variables.d.ts +0 -117
  177. package/dist/lib/substitutions-variables.d.ts.map +0 -1
  178. package/dist/lib/substitutions-variables.js.map +0 -1
  179. package/dist/lib/types.d.ts.map +0 -1
  180. package/dist/lib/types.js +0 -13
  181. package/dist/lib/types.js.map +0 -1
  182. package/dist/lib/utils.d.ts +0 -30
  183. package/dist/lib/utils.d.ts.map +0 -1
  184. package/dist/lib/utils.js +0 -70
  185. package/dist/lib/utils.js.map +0 -1
  186. package/dist/tsconfig.tsbuildinfo +0 -1
  187. package/src/lib/errors.ts +0 -29
  188. package/src/lib/functions/chmod-recursive.ts +0 -103
  189. package/src/lib/functions/perform-substitutions.ts +0 -116
  190. package/src/lib/functions/utils.ts +0 -88
  191. package/src/lib/init-template-base.ts +0 -408
  192. package/src/lib/liquid-actions.ts +0 -223
  193. package/src/lib/liquid-build-configurations.ts +0 -433
  194. package/src/lib/liquid-drop.ts +0 -99
  195. package/src/lib/liquid-engine.ts +0 -135
  196. package/src/lib/liquid-package.ts +0 -108
  197. package/src/lib/package.ts +0 -947
  198. package/src/lib/policies.ts +0 -51
  199. package/src/lib/substitutions-variables.ts +0 -177
  200. package/src/lib/types.ts +0 -109
  201. package/src/package.json +0 -3
  202. package/src/tsconfig.json +0 -10
@@ -0,0 +1,2127 @@
1
+ /*
2
+ * This file is part of the xPack project (http://xpack.github.io).
3
+ * Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved.
4
+ *
5
+ * Permission to use, copy, modify, and/or distribute this software
6
+ * for any purpose is hereby granted, under the terms of the MIT license.
7
+ *
8
+ * If a copy of the license was not distributed with this file, it can
9
+ * be obtained from https://opensource.org/license/mit.
10
+ */
11
+
12
+ // ----------------------------------------------------------------------------
13
+
14
+ import assert from 'node:assert'
15
+ import * as path from 'node:path'
16
+ import * as os from 'node:os'
17
+
18
+ import { Logger } from '@xpack/logger'
19
+
20
+ // ----------------------------------------------------------------------------
21
+
22
+ import { LiquidEngine } from './liquid-engine.js'
23
+ import {
24
+ LiquidSubstitutionsVariables,
25
+ LiquidSubstitutionsStrings,
26
+ } from '../data/substitutions-variables.js'
27
+ import { filterPath } from '../functions/filter-paths.js'
28
+ import { isJsonObject, isString } from '../functions/is-something.js'
29
+ import { getErrorMessage, hasLiquidSyntax } from '../functions/utils.js'
30
+ import { performSubstitutions } from '../functions/perform-substitutions.js'
31
+ import {
32
+ JsonBuildConfigurations,
33
+ JsonBuildConfigurationTemplate,
34
+ JsonBuildConfiguration,
35
+ JsonBuildConfigurationContent,
36
+ JsonDependencies,
37
+ JsonBuildConfigurationInherits,
38
+ } from '../types/json.js'
39
+ import { Actions, Action } from './actions.js'
40
+ import { buildFolderRelativePathPropertyName } from './data-model.js'
41
+ import { ConfigurationError } from './errors.js'
42
+ import { TemplateExpander } from './template-expander.js'
43
+
44
+ // ============================================================================
45
+
46
+ /**
47
+ * Configuration parameters for constructing a build configurations collection.
48
+ *
49
+ * @remarks
50
+ * This interface defines the required configuration for creating an
51
+ * instance of {@link BuildConfigurations}. Most properties are mandatory
52
+ * except for the optional <code>jsonBuildConfigurations</code>, which can
53
+ * be undefined if there are no build configurations defined in the package.
54
+ *
55
+ * The parameters provide the collection with access to the Liquid templating
56
+ * engine, substitution variables hierarchy, build configuration definitions
57
+ * from the package manifest, and the logger for diagnostic output during
58
+ * configuration processing.
59
+ */
60
+ export interface BuildConfigurationsConstructorParameters {
61
+ /**
62
+ * The Liquid templating engine for variable substitution.
63
+ */
64
+ engine: LiquidEngine
65
+
66
+ /**
67
+ * The variables available for substitution in configuration definitions.
68
+ */
69
+ substitutionsVariables: LiquidSubstitutionsVariables
70
+
71
+ /**
72
+ * The JSON build configurations definitions, or undefined if no build
73
+ * configurations are defined.
74
+ */
75
+ jsonBuildConfigurations: JsonBuildConfigurations | undefined
76
+
77
+ /**
78
+ * The logger instance for output and diagnostics.
79
+ */
80
+ log: Logger
81
+ }
82
+
83
+ /**
84
+ * A collection of <b>xpm</b> build configurations.
85
+ *
86
+ * @remarks
87
+ * This class manages build configurations defined in package metadata,
88
+ * including template expansion with matrix parameters and initialisation of
89
+ * derived configuration instances.
90
+ *
91
+ * Configuration lifecycle phases:
92
+ *
93
+ * <ol>
94
+ * <li><b>Construction:</b> Basic setup without processing configurations.</li>
95
+ * <li><b>Initialisation:</b> Template name expansion without content
96
+ * evaluation.</li>
97
+ * <li><b>Retrieval:</b> On-demand instantiation when accessed via
98
+ * <code>get()</code>.</li>
99
+ * <li><b>Configuration Initialisation:</b> Full processing including
100
+ * inheritance, property resolution, dependency substitution, and
101
+ * action preparation.</li>
102
+ * </ol>
103
+ *
104
+ * This lazy evaluation strategy ensures that only configurations actually
105
+ * used incur the cost of template evaluation, inheritance resolution, and
106
+ * variable substitution.
107
+ */
108
+ export class BuildConfigurations {
109
+ // --------------------------------------------------------------------------
110
+ // Public Members.
111
+
112
+ /**
113
+ * The logger instance for output and diagnostics.
114
+ *
115
+ * @remarks
116
+ * This logger provides trace-level diagnostics throughout the build
117
+ * configuration lifecycle, including template expansion, inheritance
118
+ * resolution, property merging, and dependency substitution. It enables
119
+ * detailed debugging of complex build configuration hierarchies without
120
+ * impacting runtime performance when tracing is disabled.
121
+ */
122
+ readonly log: Logger
123
+
124
+ /**
125
+ * The Liquid templating engine for variable substitution.
126
+ *
127
+ * @remarks
128
+ * This engine instance is shared across all build configurations and
129
+ * configured with custom filters for platform detection, path
130
+ * manipulation, and xpm-specific operations. It processes templates in
131
+ * configuration names, matrix parameters, properties, dependencies, and
132
+ * actions, ensuring consistent template evaluation throughout the
133
+ * configuration lifecycle.
134
+ */
135
+ readonly engine: LiquidEngine
136
+
137
+ /**
138
+ * The variables available for substitution in configuration definitions.
139
+ *
140
+ * @remarks
141
+ * This comprehensive variable hierarchy provides the base context for all
142
+ * build configuration template evaluation, extended per-configuration with
143
+ * specific properties, dependencies, and matrix parameters.
144
+ *
145
+ * Base hierarchy includes:
146
+ *
147
+ * <ol>
148
+ * <li><b>Environment variables:</b> <code>env</code> namespace with system
149
+ * environment.</li>
150
+ * <li><b>Platform detection:</b> <code>os</code> namespace with
151
+ * platform-specific values.</li>
152
+ * <li><b>Path utilities:</b> <code>path</code> namespace with path
153
+ * manipulationfunctions.</li>
154
+ * <li><b>Package metadata:</b> <code>package</code> namespace with
155
+ * name, version, dependencies.</li>
156
+ * </ol>
157
+ *
158
+ * Individual configurations extend this with their own `properties`,
159
+ * `configuration`, and `matrix` namespaces during initialisation.
160
+ */
161
+ readonly substitutionsVariables: LiquidSubstitutionsVariables
162
+
163
+ /**
164
+ * The JSON object containing build configuration definitions.
165
+ *
166
+ * @remarks
167
+ * This object holds raw build configuration definitions from the
168
+ * `package.json` `xpack.buildConfigurations` section. Configurations can be:
169
+ *
170
+ * <ol>
171
+ * <li><b>Regular configurations:</b> Direct objects with properties,
172
+ * dependencies, actions, and inheritance.</li>
173
+ * <li><b>Template configurations:</b> Objects with <code>matrix</code>
174
+ * and <code>template</code>
175
+ * properties for generating multiple configurations from a single
176
+ * definition.</li>
177
+ * </ol>
178
+ *
179
+ * Template configuration names (containing `{{` markers) trigger matrix
180
+ * expansion during initialisation, creating concrete configurations from
181
+ * the Cartesian product of matrix parameter values. Each configuration
182
+ * can inherit from others, creating complex dependency hierarchies.
183
+ */
184
+ readonly jsonBuildConfigurations: JsonBuildConfigurations
185
+
186
+ // --------------------------------------------------------------------------
187
+ // Protected Members.
188
+
189
+ /**
190
+ * Map of build configuration names to their corresponding instances.
191
+ *
192
+ * @remarks
193
+ * This map serves as the primary configuration registry, populated during
194
+ * collection initialisation with entries for all discovered configurations.
195
+ *
196
+ * Key characteristics:
197
+ *
198
+ * <ol>
199
+ * <li>Known only after <code>BuildConfigurations.initialise()</code>
200
+ * completes.</li>
201
+ * <li>Possibly empty if there are no build configurations defined.</li>
202
+ * <li>Values can be <code>undefined</code> to indicate a configuration
203
+ * exists but hasn't been instantiated yet (lazy loading).</li>
204
+ * <li>For template configurations, contains one entry per expanded
205
+ * combination, not the original template definition.</li>
206
+ * </ol>
207
+ *
208
+ * Configurations transition from `undefined` to instantiated when first
209
+ * accessed via {@link BuildConfigurations.get}, implementing the
210
+ * lazy evaluation pattern to avoid unnecessary processing.
211
+ */
212
+ protected readonly _buildConfigurationsMap: Map<
213
+ string,
214
+ BuildConfiguration | undefined
215
+ > = new Map<string, BuildConfiguration | undefined>()
216
+
217
+ /**
218
+ * Map of expanded build configuration names to their JSON source names.
219
+ *
220
+ * @remarks
221
+ * This reverse mapping enables retrieving the original configuration
222
+ * definition from `jsonBuildConfigurations` when lazy-loading
223
+ * configuration instances.
224
+ *
225
+ * Mapping behavior:
226
+ *
227
+ * <ol>
228
+ * <li><b>For regular configurations:</b> Maps configuration name to itself
229
+ * (identity mapping).</li>
230
+ * <li><b>For template configurations:</b> Maps each generated configuration
231
+ * name
232
+ * back to the original template name (e.g., <code>release-x64</code> →
233
+ * <code>release-\{\{ matrix.arch \}\}</code>).</li>
234
+ * <li>Known only after <code>BuildConfigurations.initialise()</code>
235
+ * completes.</li>
236
+ * <li>Enables <code>BuildConfigurations.get()</code> to locate the
237
+ * correct JSON definition when instantiating a configuration on
238
+ * demand.</li>
239
+ * </ol>
240
+ *
241
+ * This indirection is essential for lazy evaluation, allowing deferred
242
+ * instantiation while maintaining the connection to original definitions.
243
+ */
244
+ protected readonly _jsonBuildConfigurationsNamesMap: Map<string, string> =
245
+ new Map<string, string>()
246
+
247
+ /**
248
+ * Set of all build configuration names for duplicate detection.
249
+ *
250
+ * @remarks
251
+ * This set provides O(1) existence checks for configuration names,
252
+ * enabling efficient validation during template expansion to prevent
253
+ * duplicate configurations.
254
+ *
255
+ * Duplicate scenarios detected:
256
+ *
257
+ * <ol>
258
+ * <li>Explicit duplicates in <code>package.json</code> with identical
259
+ * names.</li>
260
+ * <li>Template expansion conflicts where different templates generate the
261
+ * same concrete configuration name.</li>
262
+ * <li>Conflicts between template-generated names and explicitly defined
263
+ * configuration names.</li>
264
+ * </ol>
265
+ *
266
+ * Detection occurs during {@link BuildConfigurations.initialise},
267
+ * throwing {@link ConfigurationError} when duplicates are found to ensure
268
+ * configuration name uniqueness.
269
+ */
270
+ protected readonly _namesSet: Set<string> = new Set<string>()
271
+
272
+ /**
273
+ * Flag indicating whether the collection has been initialised.
274
+ *
275
+ * @remarks
276
+ * This flag prevents redundant initialisation and ensures idempotent
277
+ * behavior when {@link BuildConfigurations.initialise} is called
278
+ * multiple times.
279
+ *
280
+ * State transitions:
281
+ *
282
+ * <ol>
283
+ * <li>Initially <code>false</code> after construction.</li>
284
+ * <li>Set to <code>true</code> after successful template expansion
285
+ * and configuration
286
+ * name registration.</li>
287
+ * <li>Checked at the beginning of
288
+ * <code>BuildConfigurations.initialise()</code> to return early if
289
+ * already initialised.</li>
290
+ * </ol>
291
+ *
292
+ * This pattern supports safe repeated calls during complex initialisation
293
+ * sequences without duplicating work or corrupting internal state.
294
+ */
295
+ protected _isInitialised = false
296
+
297
+ /**
298
+ * Cached array of all build configuration names in the collection.
299
+ *
300
+ * @remarks
301
+ * This array provides O(1) access to configuration names without
302
+ * repeatedly creating new arrays from the map keys, improving performance
303
+ * when the names are accessed multiple times.
304
+ *
305
+ * Key characteristics:
306
+ *
307
+ * <ol>
308
+ * <li>Empty initially after construction.</li>
309
+ * <li>Populated during
310
+ * <code>BuildConfigurations.initialise()</code> after all
311
+ * configuration names are determined.</li>
312
+ * <li>Contains all configuration names including those generated from
313
+ * templates.</li>
314
+ * <li>Returned by the <code>names</code> getter for efficient repeated
315
+ * access.</li>
316
+ * </ol>
317
+ *
318
+ * This cached approach avoids the overhead of calling
319
+ * `Array.from(map.keys())` on every access whilst still
320
+ * providing a clean getter interface.
321
+ */
322
+ protected _names: string[] = []
323
+
324
+ // --------------------------------------------------------------------------
325
+ // Constructor and async initialiser.
326
+
327
+ /**
328
+ * Constructs a build configurations collection.
329
+ *
330
+ * @remarks
331
+ * The constructor performs partial initialisation. Complete
332
+ * initialisation requires calling
333
+ * {@link BuildConfigurations.initialise}.
334
+ *
335
+ * @param engine - The Liquid templating engine for variable substitution.
336
+ * @param substitutionsVariables - The variables available for substitution.
337
+ * @param jsonBuildConfigurations - The JSON build configurations definitions,
338
+ * or undefined if no build configurations are defined.
339
+ * @param log - The logger instance for output and diagnostics.
340
+ */
341
+ constructor({
342
+ engine,
343
+ substitutionsVariables,
344
+ jsonBuildConfigurations,
345
+ log,
346
+ }: BuildConfigurationsConstructorParameters) {
347
+ assert(log, 'log is required')
348
+ assert(engine, 'engine is required')
349
+ assert(substitutionsVariables, 'substitutionsVariables is required')
350
+
351
+ log.trace(`${BuildConfigurations.name}()`)
352
+
353
+ this.log = log
354
+ this.engine = engine
355
+ this.substitutionsVariables = substitutionsVariables
356
+ this.jsonBuildConfigurations = jsonBuildConfigurations ?? {}
357
+
358
+ // log.trace('substitutionsVariables => ', this.substitutionsVariables)
359
+ }
360
+
361
+ /**
362
+ * Completes the async initialisation of the build configurations collection.
363
+ *
364
+ * @remarks
365
+ * This method implements the first step of lazy evaluation. It processes
366
+ * all build configuration definitions by expanding template configuration
367
+ * names based on matrix parameters, but does not evaluate the configuration
368
+ * content or perform Liquid substitutions. The actual template evaluation
369
+ * and variable substitution occur later when individual configurations are
370
+ * initialised via {@link BuildConfiguration.initialise}, and only
371
+ * for configurations that are actually used. This approach avoids unnecessary
372
+ * operations on unused configurations.
373
+ *
374
+ * Processing steps:
375
+ *
376
+ * <ol>
377
+ * <li>Return early if already initialised (idempotent behaviour).</li>
378
+ * <li>Iterate through all build configuration definitions from the JSON
379
+ * object.</li>
380
+ * <li>For template configurations (names containing <code>\{\{</code>):
381
+ * <ul>
382
+ * <li>Call <code>_processTemplate()</code> to expand and register all
383
+ * generated configurations.</li>
384
+ * </ul>
385
+ * </li>
386
+ * <li>For regular configurations:
387
+ * <ul>
388
+ * <li>Validate uniqueness of the configuration name.</li>
389
+ * <li>Register the configuration in internal maps with
390
+ * <code>undefined</code> value (lazy loading).</li>
391
+ * </ul>
392
+ * </li>
393
+ * <li>Cache the array of all configuration names for efficient repeated
394
+ * access.</li>
395
+ * </ol>
396
+ *
397
+ * @returns A promise that resolves to `true` if initialisation was performed,
398
+ * or `false` if already initialised.
399
+ *
400
+ * @throws {@link ConfigurationError}
401
+ * If duplicate names are detected or template expansion fails.
402
+ */
403
+ async initialise(): Promise<boolean> {
404
+ const log = this.log
405
+
406
+ if (this._isInitialised) {
407
+ log.trace(`${BuildConfigurations.name}.initialise() again`)
408
+ return false
409
+ }
410
+
411
+ log.trace(`${BuildConfigurations.name}.initialise()`)
412
+
413
+ for (const [
414
+ buildConfigurationName,
415
+ jsonBuildConfiguration,
416
+ ] of Object.entries(this.jsonBuildConfigurations)) {
417
+ if (hasLiquidSyntax(buildConfigurationName)) {
418
+ await this._processTemplate({
419
+ buildConfigurationName,
420
+ jsonBuildConfigurationTemplate:
421
+ jsonBuildConfiguration as JsonBuildConfigurationTemplate,
422
+ })
423
+ } else {
424
+ if (this._namesSet.has(buildConfigurationName)) {
425
+ throw new ConfigurationError(
426
+ `build configuration name ` +
427
+ `"${buildConfigurationName}" already defined`
428
+ )
429
+ } else {
430
+ this._buildConfigurationsMap.set(buildConfigurationName, undefined)
431
+ this._jsonBuildConfigurationsNamesMap.set(
432
+ buildConfigurationName,
433
+ buildConfigurationName
434
+ )
435
+ this._namesSet.add(buildConfigurationName)
436
+ }
437
+ }
438
+ }
439
+
440
+ const names = Array.from(this._buildConfigurationsMap.keys())
441
+ this._names = names
442
+
443
+ log.trace(`${BuildConfigurations.name}.initialise() =>`, names)
444
+
445
+ this._isInitialised = true
446
+ return true
447
+ }
448
+
449
+ // --------------------------------------------------------------------------
450
+ // Public Methods.
451
+
452
+ /**
453
+ * The number of build configurations in the collection.
454
+ *
455
+ * @remarks
456
+ * This value is known only after `initialise()`.
457
+ *
458
+ * This getter provides direct access to the collection size, enabling
459
+ * callers to check for emptiness or iterate with knowledge of the
460
+ * collection's extent.
461
+ *
462
+ * @returns The number of build configurations in the collection.
463
+ */
464
+ get size(): number {
465
+ assert(
466
+ this._isInitialised,
467
+ 'BuildConfigurations collection must be initialised before ' +
468
+ 'accessing size'
469
+ )
470
+
471
+ return this._buildConfigurationsMap.size
472
+ }
473
+
474
+ /**
475
+ * Indicates whether the collection is empty.
476
+ *
477
+ * @remarks
478
+ * This value is known only after `initialise()`.
479
+ *
480
+ * @returns `true` if there are no build configurations, `false` otherwise.
481
+ */
482
+ get isEmpty(): boolean {
483
+ assert(
484
+ this._isInitialised,
485
+ 'BuildConfigurations collection must be initialised before ' +
486
+ 'accessing isEmpty'
487
+ )
488
+
489
+ return this._buildConfigurationsMap.size === 0
490
+ }
491
+
492
+ /**
493
+ * The names of all build configurations.
494
+ *
495
+ * @remarks
496
+ * This value is known only after `initialise()`.
497
+ *
498
+ * This getter returns the cached array of configuration names for
499
+ * efficient repeated access without recreating the array.
500
+ *
501
+ * @returns An array of build configuration names.
502
+ */
503
+ get names(): string[] {
504
+ assert(
505
+ this._isInitialised,
506
+ 'BuildConfigurations collection must be initialised before ' +
507
+ 'accessing names'
508
+ )
509
+
510
+ return this._names
511
+ }
512
+
513
+ /**
514
+ * Retrieves the JSON configuration name for a build configuration.
515
+ *
516
+ * @param buildConfigurationName - The build configuration name to resolve.
517
+ * @returns The JSON configuration name associated with the given build
518
+ * configuration name.
519
+ *
520
+ * @remarks
521
+ * For template-generated configurations, this returns the template
522
+ * name.
523
+ *
524
+ * @throws {@link InputError}
525
+ * If the build configuration does not exist.
526
+ */
527
+ getJsonName(buildConfigurationName: string): string {
528
+ assert(
529
+ this._isInitialised,
530
+ 'BuildConfigurations collection must be initialised before ' +
531
+ 'accessing getJsonName()'
532
+ )
533
+
534
+ const name = this._jsonBuildConfigurationsNamesMap.get(
535
+ buildConfigurationName
536
+ )
537
+ if (name === undefined) {
538
+ throw new ConfigurationError(
539
+ `build configuration "${buildConfigurationName}" does not exist`
540
+ )
541
+ }
542
+
543
+ return name
544
+ }
545
+
546
+ /**
547
+ * Determines whether a JSON definition exists for a build configuration.
548
+ *
549
+ * @param buildConfigurationName - The build configuration name to check.
550
+ * @returns `true` if a JSON definition exists, `false` otherwise.
551
+ */
552
+ hasJson(buildConfigurationName: string): boolean {
553
+ assert(
554
+ this._isInitialised,
555
+ 'BuildConfigurations collection must be initialised before ' +
556
+ 'accessing hasJson()'
557
+ )
558
+
559
+ return this._jsonBuildConfigurationsNamesMap.has(buildConfigurationName)
560
+ }
561
+
562
+ /**
563
+ * Retrieves the JSON build configuration definition.
564
+ *
565
+ * @param buildConfigurationName - The build configuration name to resolve.
566
+ * @returns The JSON build configuration definition.
567
+ */
568
+ getJson(buildConfigurationName: string): JsonBuildConfiguration {
569
+ assert(
570
+ this._isInitialised,
571
+ 'BuildConfigurations collection must be initialised before ' +
572
+ 'accessing getJson()'
573
+ )
574
+
575
+ return this.jsonBuildConfigurations[
576
+ this.getJsonName(buildConfigurationName)
577
+ ]
578
+ }
579
+
580
+ /**
581
+ * Determines whether a build configuration is hidden.
582
+ *
583
+ * @param buildConfigurationName - The build configuration name to check.
584
+ * @returns `true` if the configuration is hidden, `false` otherwise.
585
+ */
586
+ isHidden(buildConfigurationName: string): boolean {
587
+ assert(
588
+ this._isInitialised,
589
+ 'BuildConfigurations collection must be initialised before ' +
590
+ 'accessing isHidden()'
591
+ )
592
+
593
+ const jsonBuildConfigurationName = this.getJsonName(buildConfigurationName)
594
+ if (jsonBuildConfigurationName.includes('{{')) {
595
+ const jsonBuildConfigurationTemplate: JsonBuildConfigurationTemplate =
596
+ this.jsonBuildConfigurations[
597
+ jsonBuildConfigurationName
598
+ ] as JsonBuildConfigurationTemplate
599
+ return jsonBuildConfigurationTemplate.template.hidden ?? false
600
+ }
601
+
602
+ const jsonBuildConfigurationContent: JsonBuildConfigurationContent = this
603
+ .jsonBuildConfigurations[
604
+ jsonBuildConfigurationName
605
+ ] as JsonBuildConfigurationContent
606
+ return jsonBuildConfigurationContent.hidden ?? false
607
+ }
608
+
609
+ /**
610
+ * Determines whether a build configuration exists in the collection.
611
+ *
612
+ * @param buildConfigurationName - The build configuration name to check.
613
+ * @returns `true` if the configuration exists, `false` otherwise.
614
+ */
615
+ has(buildConfigurationName: string): boolean {
616
+ assert(
617
+ this._isInitialised,
618
+ 'BuildConfigurations collection must be initialised before ' +
619
+ 'accessing has()'
620
+ )
621
+
622
+ return this._buildConfigurationsMap.has(buildConfigurationName)
623
+ }
624
+
625
+ /**
626
+ * Retrieves a build configuration by name, creating it if required.
627
+ *
628
+ * @remarks
629
+ * This method implements lazy evaluation to avoid unnecessary
630
+ * operations. Build configurations are instantiated on demand but
631
+ * remain uninitialised until actually used.
632
+ *
633
+ * Retrieval process:
634
+ *
635
+ * <ol>
636
+ * <li>Check if the configuration already exists in the internal map.</li>
637
+ * <li>If found and already instantiated, return the existing instance.</li>
638
+ * <li>If the configuration name is unknown (not in JSON name mapping),
639
+ * throw <code>InputError</code>.</li>
640
+ * <li>For known but not yet instantiated configurations:
641
+ * <ul>
642
+ * <li>Resolve the original JSON configuration name (handles both
643
+ * regular and template-generated configurations).</li>
644
+ * <li>Retrieve the JSON configuration definition.</li>
645
+ * <li>Create a new <code>BuildConfiguration</code> instance.</li>
646
+ * <li>Store the instance in the map for future access.</li>
647
+ * </ul>
648
+ * </li>
649
+ * <li>Return the configuration instance (still uninitialised).</li>
650
+ * </ol>
651
+ *
652
+ * The two-step lazy evaluation process:
653
+ *
654
+ * <ol>
655
+ * <li>During collection initialisation
656
+ * (<code>BuildConfigurations.initialise()</code>), only the
657
+ * matrix of options is evaluated for each template, expanding
658
+ * configuration names without processing their content.</li>
659
+ * <li>Later, when a configuration is accessed via this method and
660
+ * subsequently initialised
661
+ * (<code>BuildConfiguration.initialise()</code>), the template
662
+ * is fully evaluated and Liquid substitutions are performed on
663
+ * all properties.</li>
664
+ * </ol>
665
+ *
666
+ * This approach ensures that only build configurations that are
667
+ * actually used incur the cost of template evaluation and variable
668
+ * substitution.
669
+ *
670
+ * @param buildConfigurationName - The build configuration name to retrieve.
671
+ * @returns The build configuration instance.
672
+ *
673
+ * @throws {@link InputError}
674
+ * If a configuration with the specified name does not exist.
675
+ */
676
+ get(buildConfigurationName: string): BuildConfiguration {
677
+ assert(
678
+ this._isInitialised,
679
+ 'BuildConfigurations collection must be initialised before ' +
680
+ 'accessing get()'
681
+ )
682
+
683
+ const log = this.log
684
+ log.trace(`${BuildConfigurations.name}.get(${buildConfigurationName})`)
685
+
686
+ let buildConfiguration = this._buildConfigurationsMap.get(
687
+ buildConfigurationName
688
+ )
689
+ if (buildConfiguration === undefined) {
690
+ // This will throw InputError if the configuration doesn't exist
691
+ const jsonBuildConfigurationName: string = this.getJsonName(
692
+ buildConfigurationName
693
+ )
694
+
695
+ // Safety net: This fallback to empty object is defensive programming.
696
+ // The jsonBuildConfigurations[jsonBuildConfigurationName] should always
697
+ // be defined because getJsonName() throws if the configuration doesn't
698
+ // exist. The ?? {} provides protection against unexpected inconsistencies
699
+ // between the names map and the configurations object.
700
+ /* c8 ignore start - safety net, they are always defined */
701
+ const jsonBuildConfiguration: JsonBuildConfigurationContent = (this
702
+ .jsonBuildConfigurations[jsonBuildConfigurationName] ??
703
+ {}) as JsonBuildConfigurationContent
704
+ /* c8 ignore stop */
705
+
706
+ buildConfiguration = new BuildConfiguration({
707
+ buildConfigurationName,
708
+ jsonBuildConfiguration,
709
+ parentBuildConfigurations: this,
710
+ })
711
+ this._buildConfigurationsMap.set(
712
+ buildConfigurationName,
713
+ buildConfiguration
714
+ )
715
+ }
716
+
717
+ // await buildConfiguration.initialise()
718
+ return buildConfiguration
719
+ }
720
+
721
+ // --------------------------------------------------------------------------
722
+ // Private Methods.
723
+
724
+ /**
725
+ * Processes a template build configuration by expanding it and registering
726
+ * the generated configurations.
727
+ *
728
+ * @remarks
729
+ * This helper method is called during collection initialisation for each
730
+ * build configuration whose name contains template syntax
731
+ * (<code>\{\{</code> markers).
732
+ *
733
+ * Processing steps:
734
+ *
735
+ * <ol>
736
+ * <li>Calls <code>_expandTemplateBuildConfigurations()</code> to generate
737
+ * all configuration instances from the template's matrix parameters.</li>
738
+ * <li>Validates that each expanded configuration name is unique and does
739
+ * not conflict with existing configurations.</li>
740
+ * <li>Registers each expanded configuration in the internal maps:
741
+ * <ul>
742
+ * <li><code>_buildConfigurationsMap</code>: Maps name to configuration
743
+ * instance.</li>
744
+ * <li><code>_jsonBuildConfigurationsNamesMap</code>: Maps expanded name
745
+ * back to original template name.</li>
746
+ * <li><code>_namesSet</code>: Tracks all registered
747
+ * names for duplicate detection.</li>
748
+ * </ul>
749
+ * </li>
750
+ * </ol>
751
+ *
752
+ * @param buildConfigurationName - The template configuration name
753
+ * containing Liquid variables.
754
+ * @param jsonBuildConfiguration - The JSON template definition containing
755
+ * matrix parameters and a configuration template.
756
+ * @returns A promise that resolves when processing is complete.
757
+ *
758
+ * @throws {@link ConfigurationError}
759
+ * If duplicate configuration names are detected during expansion or if
760
+ * template expansion fails.
761
+ */
762
+ protected async _processTemplate({
763
+ buildConfigurationName,
764
+ jsonBuildConfigurationTemplate,
765
+ }: {
766
+ buildConfigurationName: string
767
+ jsonBuildConfigurationTemplate: JsonBuildConfigurationTemplate
768
+ }): Promise<void> {
769
+ // Expand templates and generate multiple build configurations.
770
+ try {
771
+ const expandedBuildConfigurationsMap =
772
+ await this._expandTemplateBuildConfigurations({
773
+ buildConfigurationName,
774
+ jsonBuildConfigurationTemplate,
775
+ })
776
+ for (const [
777
+ expandedBuildConfigurationName,
778
+ expandedBuildConfiguration,
779
+ ] of expandedBuildConfigurationsMap) {
780
+ if (this._namesSet.has(expandedBuildConfigurationName)) {
781
+ throw new ConfigurationError(
782
+ `duplicate build configuration name ` +
783
+ `"${expandedBuildConfigurationName}" ` +
784
+ `could not be generated from template.`
785
+ )
786
+ } else {
787
+ this._buildConfigurationsMap.set(
788
+ expandedBuildConfigurationName,
789
+ expandedBuildConfiguration
790
+ )
791
+ this._jsonBuildConfigurationsNamesMap.set(
792
+ expandedBuildConfigurationName,
793
+ buildConfigurationName
794
+ )
795
+ this._namesSet.add(expandedBuildConfigurationName)
796
+ }
797
+ }
798
+ } catch (error) {
799
+ const message =
800
+ getErrorMessage(error) +
801
+ ` in buildConfiguration "${buildConfigurationName}"`
802
+ throw new ConfigurationError(message)
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Expands a template build configuration into multiple configurations.
808
+ *
809
+ * @remarks
810
+ * This method uses the {@link TemplateExpander} to compute the Cartesian
811
+ * product of matrix parameter values and creates a configuration for each
812
+ * combination, substituting matrix values into both the configuration name
813
+ * and content.
814
+ *
815
+ * Processing steps:
816
+ *
817
+ * <ol>
818
+ * <li>Validates matrix and template structure.</li>
819
+ * <li>Delegates to <code>TemplateExpander</code> for matrix processing and
820
+ * name expansion.</li>
821
+ * <li>Creates configuration instances via factory callback for each
822
+ * combination.</li>
823
+ * </ol>
824
+ *
825
+ * Matrix variables are scoped to individual configurations and accessible
826
+ * via the `matrix` namespace during property, dependency, and action
827
+ * evaluation.
828
+ *
829
+ * @param buildConfigurationName - The template configuration name containing
830
+ * Liquid variables.
831
+ * @param jsonBuildConfigurationTemplate - The template definition containing
832
+ * matrix parameters and a configuration template.
833
+ * @returns A promise that resolves to a map of expanded configuration names
834
+ * to their corresponding instances.
835
+ *
836
+ * @throws {@link ConfigurationError}
837
+ * If the matrix structure is invalid or substitution fails.
838
+ */
839
+ protected async _expandTemplateBuildConfigurations({
840
+ buildConfigurationName,
841
+ jsonBuildConfigurationTemplate,
842
+ }: {
843
+ buildConfigurationName: string
844
+ jsonBuildConfigurationTemplate: JsonBuildConfigurationTemplate
845
+ }): Promise<Map<string, BuildConfiguration>> {
846
+ const log = this.log
847
+ log.trace(
848
+ `${BuildConfigurations.name}.` +
849
+ `#expandTemplateBuildConfigurations(${buildConfigurationName})`
850
+ )
851
+
852
+ // Validate template structure
853
+ if (!isJsonObject(jsonBuildConfigurationTemplate.matrix)) {
854
+ throw new ConfigurationError(
855
+ `buildConfiguration "${buildConfigurationName}" ` +
856
+ `matrix is not an object`
857
+ )
858
+ }
859
+ if (!isJsonObject(jsonBuildConfigurationTemplate.template)) {
860
+ throw new ConfigurationError(
861
+ `buildConfiguration "${buildConfigurationName}" ` +
862
+ `template is not a JSON object`
863
+ )
864
+ }
865
+
866
+ // Use TemplateExpander for matrix processing and expansion
867
+ const expander = new TemplateExpander<
868
+ JsonBuildConfigurationContent,
869
+ BuildConfiguration
870
+ >({
871
+ engine: this.engine,
872
+ substitutionsVariables: this.substitutionsVariables,
873
+ log: this.log,
874
+ })
875
+
876
+ return await expander.expandTemplate({
877
+ templateName: buildConfigurationName,
878
+ matrix: jsonBuildConfigurationTemplate.matrix,
879
+ templateContent: jsonBuildConfigurationTemplate.template,
880
+ templateType: 'buildConfiguration',
881
+ instanceFactory: (
882
+ expandedName: string,
883
+ combination: Record<string, string>,
884
+ templateContent: JsonBuildConfigurationContent,
885
+ originalTemplateName: string
886
+ ) =>
887
+ new BuildConfiguration({
888
+ buildConfigurationName: expandedName,
889
+ templateBuildConfigurationName: originalTemplateName,
890
+ jsonBuildConfiguration: templateContent,
891
+ parentBuildConfigurations: this,
892
+ matrixParameters: { ...combination },
893
+ }),
894
+ })
895
+ }
896
+ }
897
+
898
+ // ============================================================================
899
+
900
+ /**
901
+ * Configuration parameters for constructing a build configuration instance.
902
+ *
903
+ * @remarks
904
+ * This interface defines the required configuration for creating an
905
+ * instance of {@link BuildConfiguration}. Most properties are mandatory
906
+ * except for the optional <code>templateBuildConfigurationName</code> and
907
+ * <code>matrixParameters</code>, which are only needed for template-generated
908
+ * configurations created from matrix expansion.
909
+ *
910
+ * The parameters provide the configuration with its identity (name,
911
+ * optional template name), the JSON configuration definition, access to
912
+ * the parent collection for shared resources, and optional matrix parameter
913
+ * values for template-generated configurations.
914
+ */
915
+ export interface BuildConfigurationConstructorParameters {
916
+ /**
917
+ * The configuration name after substitution.
918
+ */
919
+ buildConfigurationName: string
920
+
921
+ /**
922
+ * The template configuration name, if derived from a template.
923
+ */
924
+ templateBuildConfigurationName?: string
925
+
926
+ /**
927
+ * The JSON configuration definition.
928
+ */
929
+ jsonBuildConfiguration: JsonBuildConfigurationContent
930
+
931
+ /**
932
+ * The parent configurations collection.
933
+ */
934
+ parentBuildConfigurations: BuildConfigurations
935
+
936
+ /**
937
+ * Optional matrix parameter values for template-generated configurations.
938
+ */
939
+ matrixParameters?: LiquidSubstitutionsStrings
940
+ }
941
+
942
+ /**
943
+ * An individual <b>xpm</b> build configuration.
944
+ *
945
+ * @remarks
946
+ * Build configurations are initialised lazily and may inherit
947
+ * properties, dependencies, and actions from other configurations.
948
+ *
949
+ * A configuration can exist in three states:
950
+ *
951
+ * <ol>
952
+ * <li><b>Undefined:</b> Name is known but instance not yet created.</li>
953
+ * <li><b>Instantiated:</b> Object exists but not yet fully processed.</li>
954
+ * <li><b>Initialised:</b> Inheritance resolved, properties evaluated,
955
+ * dependencies substituted, and actions prepared.</li>
956
+ * </ol>
957
+ *
958
+ * Inheritance is processed recursively with circular reference detection.
959
+ * Later inherited properties override earlier ones, and local properties
960
+ * override all inherited ones. Dependencies and actions are merged from
961
+ * all inherited configurations.
962
+ */
963
+ export class BuildConfiguration {
964
+ // --------------------------------------------------------------------------
965
+ // Public Members.
966
+
967
+ /**
968
+ * The build configuration name after substitution.
969
+ *
970
+ * @remarks
971
+ * This is the final, expanded configuration name used for identification
972
+ * and selection. For template-generated configurations, this is the
973
+ * concrete name after matrix substitution (e.g., `release-x64` rather than
974
+ * `release-{{ matrix.arch }}`).
975
+ *
976
+ * The name is used for:
977
+ *
978
+ * <ol>
979
+ * <li>User-facing identification when listing or selecting
980
+ * configurations.</li>
981
+ * <li><b>Build folder path generation (default:</b>
982
+ * <code>build/\{name\}</code>).</li>
983
+ * <li>Logging and diagnostic output to track configuration lifecycle.</li>
984
+ * <li>Inheritance references from other configurations.</li>
985
+ * </ol>
986
+ *
987
+ * Names must be unique within the configurations collection, enforced
988
+ * during {@link BuildConfigurations.initialise}.
989
+ */
990
+ readonly name: string
991
+
992
+ /**
993
+ * The template build configuration name, if derived from a template.
994
+ *
995
+ * @remarks
996
+ * For template-generated configurations, this preserves the original
997
+ * template name containing Liquid variables (e.g.,
998
+ * `release-{{ matrix.arch }}`), while `buildConfigurationName` holds the
999
+ * expanded concrete name.
1000
+ *
1001
+ * Usage:
1002
+ *
1003
+ * <ol>
1004
+ * <li>Undefined for regular (non-template) configurations.</li>
1005
+ * <li>Set to the template name for configurations generated from matrix
1006
+ * expansion.</li>
1007
+ * <li>Used to determine whether full JSON substitution is needed during
1008
+ * initialisation (templates require complete substitution, regular
1009
+ * configurations only substitute specific fields).</li>
1010
+ * <li>Enables tracing and debugging of template expansion process.</li>
1011
+ * </ol>
1012
+ */
1013
+ readonly templateName?: string
1014
+
1015
+ /**
1016
+ * The parent build configurations collection.
1017
+ *
1018
+ * @remarks
1019
+ * This reference maintains the hierarchical relationship between
1020
+ * individual configurations and their containing collection, providing
1021
+ * essential context for configuration initialisation.
1022
+ *
1023
+ * The parent collection provides access to:
1024
+ *
1025
+ * <ol>
1026
+ * <li>Liquid templating engine for variable substitution.</li>
1027
+ * <li>Base substitution variables hierarchy (package metadata,
1028
+ * environment, platform detection).</li>
1029
+ * <li>Logger instance for diagnostic output.</li>
1030
+ * <li>JSON build configurations lookup for inheritance resolution.</li>
1031
+ * <li>Other configuration instances when processing inheritance chains.</li>
1032
+ * </ol>
1033
+ *
1034
+ * This design enables configurations to access shared resources without
1035
+ * duplicating them, while supporting complex inheritance relationships
1036
+ * where configurations reference and inherit from each other.
1037
+ */
1038
+ readonly parentBuildConfigurations: BuildConfigurations
1039
+
1040
+ /**
1041
+ * The list of inherited configuration names.
1042
+ *
1043
+ * @remarks
1044
+ * This array specifies the inheritance chain for this configuration,
1045
+ * processed sequentially during initialisation with later entries
1046
+ * overriding earlier ones.
1047
+ *
1048
+ * Inheritance processing:
1049
+ *
1050
+ * <ol>
1051
+ * <li>Populated from <code>inherits</code> or deprecated
1052
+ * <code>inherit</code> field during
1053
+ * initialisation.</li>
1054
+ * <li>Supports both string (single parent) and array (multiple parents)
1055
+ * formats.</li>
1056
+ * <li>Each inherited configuration is initialised recursively before
1057
+ * merging its properties, dependencies, and actions.</li>
1058
+ * <li>Circular references are detected and rejected with
1059
+ * <code>InputError</code>.</li>
1060
+ * <li>Later inherited configurations override properties from earlier
1061
+ * ones, and local properties override all inherited ones.</li>
1062
+ * </ol>
1063
+ */
1064
+ inheritsNames: string[] = []
1065
+
1066
+ /**
1067
+ * Indicates whether the configuration is hidden.
1068
+ *
1069
+ * @remarks
1070
+ * Hidden configurations are used for inheritance bases or intermediate
1071
+ * configurations that shouldn't be directly selected for building.
1072
+ *
1073
+ * Effects of hidden status:
1074
+ *
1075
+ * <ol>
1076
+ * <li>Hidden configurations don't compute build folder relative paths
1077
+ * during initialisation (optimization for inheritance-only configs).</li>
1078
+ * <li>May be excluded from user-facing configuration lists depending on
1079
+ * application logic.</li>
1080
+ * <li>Still fully initialised and available for inheritance by other
1081
+ * configurations.</li>
1082
+ * <li>Derived from <code>hidden</code> field in JSON configuration definition
1083
+ * (defaults to <code>false</code>).</li>
1084
+ * </ol>
1085
+ *
1086
+ * Common use case:
1087
+ *
1088
+ * Base configurations that define common properties,
1089
+ * dependencies, or actions inherited by multiple concrete configurations.
1090
+ */
1091
+ readonly isHidden: boolean
1092
+
1093
+ /**
1094
+ * The resolved properties for this configuration.
1095
+ *
1096
+ * @remarks
1097
+ * This object contains the final merged properties after inheritance
1098
+ * resolution and becomes available in the `properties` namespace for
1099
+ * Liquid template substitution.
1100
+ *
1101
+ * Property resolution order:
1102
+ *
1103
+ * <ol>
1104
+ * <li>Start with empty object.</li>
1105
+ * <li>Merge properties from each inherited configuration in sequence
1106
+ * (later overrides earlier).</li>
1107
+ * <li>Merge local properties from JSON definition (overrides all
1108
+ * inherited).</li>
1109
+ * <li>Add computed <code>buildFolderRelativePath</code> property
1110
+ * for non-hidden
1111
+ * configurations.</li>
1112
+ * </ol>
1113
+ *
1114
+ * Properties are accessible in templates as `{{ properties.key }}` and
1115
+ * commonly used for compiler flags, toolchain paths, optimization
1116
+ * settings, and build-specific configuration values.
1117
+ */
1118
+ properties: LiquidSubstitutionsStrings = {}
1119
+
1120
+ /**
1121
+ * The resolved dependencies after substitutions.
1122
+ *
1123
+ * @remarks
1124
+ * This object contains the final merged dependencies after inheritance
1125
+ * resolution and Liquid template substitution.
1126
+ *
1127
+ * Dependency resolution workflow:
1128
+ *
1129
+ * <ol>
1130
+ * <li>Start with empty object.</li>
1131
+ * <li>Merge <code>dependencies</code> from each inherited configuration
1132
+ * in sequence
1133
+ * (later overrides earlier).</li>
1134
+ * <li>Merge local <code>dependencies</code> from JSON definition.</li>
1135
+ * <li>Perform Liquid template substitution on the entire
1136
+ * <code>dependencies</code>
1137
+ * object with full configuration context (properties, matrix, etc.).</li>
1138
+ * </ol>
1139
+ *
1140
+ * This enables configuration-specific dependencies with dynamic version
1141
+ * ranges or package selection based on matrix parameters, platform
1142
+ * detection, or configuration properties.
1143
+ */
1144
+ dependencies: JsonDependencies = {}
1145
+
1146
+ /**
1147
+ * The resolved development dependencies after substitutions.
1148
+ *
1149
+ * @remarks
1150
+ * This object contains the final merged development dependencies after
1151
+ * inheritance resolution and Liquid template substitution.
1152
+ *
1153
+ * Resolution workflow mirrors `dependencies`:
1154
+ *
1155
+ * <ol>
1156
+ * <li>Start with empty object.</li>
1157
+ * <li>Merge <code>devDependencies</code> from each inherited configuration
1158
+ * in sequence
1159
+ * (later overrides earlier).</li>
1160
+ * <li>Merge local <code>devDependencies</code> from JSON definition.</li>
1161
+ * <li>Perform Liquid template substitution on the entire
1162
+ * <code>devDependencies</code>
1163
+ * object with full configuration context.</li>
1164
+ * </ol>
1165
+ *
1166
+ * Typical use: Test frameworks, build tools, or debugging utilities
1167
+ * specific to certain configurations (e.g., debug builds might include
1168
+ * additional analysis tools).
1169
+ */
1170
+ devDependencies: JsonDependencies = {}
1171
+
1172
+ /**
1173
+ * The JSON build configuration content from package metadata.
1174
+ *
1175
+ * @remarks
1176
+ * This holds the raw configuration definition as it appears in
1177
+ * `package.json`, before inheritance resolution and variable substitution.
1178
+ *
1179
+ * The definition is preserved to:
1180
+ *
1181
+ * <ol>
1182
+ * <li>Enable external modification (e.g., <code>xpm uninstall</code>
1183
+ * updates this
1184
+ * directly).</li>
1185
+ * <li>Support deferred template evaluation during
1186
+ * <code>BuildConfiguration.initialise()</code>.</li>
1187
+ * <li>Provide the source for inheritance when other configurations
1188
+ * reference this one.</li>
1189
+ * <li>Allow re-evaluation with different variable contexts if needed.</li>
1190
+ * </ol>
1191
+ *
1192
+ * This immutable storage ensures configurations can be safely referenced
1193
+ * during inheritance resolution without side effects.
1194
+ */
1195
+ jsonBuildConfiguration: JsonBuildConfigurationContent
1196
+
1197
+ /**
1198
+ * Indicates whether this configuration originates from a template.
1199
+ *
1200
+ * @remarks
1201
+ * This flag determines the substitution strategy during configuration
1202
+ * initialisation, with template configurations requiring more extensive
1203
+ * processing.
1204
+ *
1205
+ * Template vs regular configuration processing:
1206
+ *
1207
+ * <ol>
1208
+ * <li>Template configurations (<code>isTemplate === true</code>):
1209
+ * <ul>
1210
+ * <li>Entire JSON configuration is stringified and substituted.</li>
1211
+ * <li>Matrix parameters available throughout all fields.</li>
1212
+ * <li>More expensive but supports matrix references anywhere.</li>
1213
+ * </ul>
1214
+ * </li>
1215
+ * <li>Regular configurations (<code>isTemplate === false</code>):
1216
+ * <ul>
1217
+ * <li>Only <code>inherits</code> field is substituted initially.</li>
1218
+ * <li>Other fields processed selectively during inheritance
1219
+ * resolution.</li>
1220
+ * <li>More efficient for configurations without matrix parameters.</li>
1221
+ * </ul>
1222
+ * </li>
1223
+ * </ol>
1224
+ *
1225
+ * Set to `true` when `templateBuildConfigurationName` is defined,
1226
+ * indicating the configuration was generated from a template expansion.
1227
+ */
1228
+ isTemplate: boolean
1229
+
1230
+ // --------------------------------------------------------------------------
1231
+ // Protected Members.
1232
+
1233
+ /**
1234
+ * The logger instance for output and diagnostics.
1235
+ *
1236
+ * @remarks
1237
+ * This logger provides trace-level diagnostics throughout the build
1238
+ * configuration lifecycle, including template substitution, inheritance
1239
+ * resolution, property merging, dependency substitution, and action
1240
+ * preparation.
1241
+ *
1242
+ * It is initialised in the constructor from the parent collection's logger
1243
+ * and used consistently across all helper methods to maintain coherent
1244
+ * logging output. This enables detailed debugging of complex configuration
1245
+ * hierarchies without impacting runtime performance when tracing is
1246
+ * disabled.
1247
+ */
1248
+ protected readonly _log: Logger
1249
+
1250
+ /**
1251
+ * The variables used for substitution in this configuration.
1252
+ *
1253
+ * @remarks
1254
+ * This extended variable hierarchy combines the base collection variables
1255
+ * with configuration-specific context, enabling accurate template
1256
+ * evaluation.
1257
+ *
1258
+ * Extension hierarchy:
1259
+ *
1260
+ * <ol>
1261
+ * <li>Starts with parent collection's base variables (env, os, path,
1262
+ * package).</li>
1263
+ * <li>Extended with <code>properties</code>: Merged from inheritance
1264
+ * chain and local
1265
+ * properties.</li>
1266
+ * <li>Extended with <code>matrix</code>: Parameter values for
1267
+ * template-generated
1268
+ * configurations.</li>
1269
+ * <li>Extended with <code>configuration</code>: The configuration
1270
+ * object itself
1271
+ * (name, dependencies, properties) accessible for self-reference.</li>
1272
+ * </ol>
1273
+ *
1274
+ * This complete context is used for all substitutions within the
1275
+ * configuration: properties, dependencies, devDependencies, and actions.
1276
+ */
1277
+ protected _substitutionsVariables: LiquidSubstitutionsVariables
1278
+
1279
+ /**
1280
+ * The matrix parameter values for template-generated configurations.
1281
+ *
1282
+ * @remarks
1283
+ * For template-generated configurations, this object contains the specific
1284
+ * matrix parameter values that produced this configuration instance from
1285
+ * the template.
1286
+ *
1287
+ * Usage pattern:
1288
+ *
1289
+ * <ol>
1290
+ * <li>Undefined for regular (non-template) configurations.</li>
1291
+ * <li>For template configurations, contains key-value pairs from the matrix
1292
+ * combination (e.g.,
1293
+ * <code>\{ arch: 'x64', optimize: 'speed' \}</code>).</li>
1294
+ * <li>Merged into substitution variables during initialisation, making
1295
+ * values accessible via the <code>matrix</code> namespace throughout the
1296
+ * configuration.</li>
1297
+ * <li>Used in configuration name substitution, property values,
1298
+ * dependencies, and action commands.</li>
1299
+ * </ol>
1300
+ *
1301
+ * Example: A template `release-{{ matrix.arch }}` with matrix parameters
1302
+ * `{ arch: 'x64' }` becomes the concrete configuration `release-x64`.
1303
+ */
1304
+ protected readonly matrixParameters?: LiquidSubstitutionsStrings
1305
+
1306
+ /**
1307
+ * The actions associated with this build configuration.
1308
+ *
1309
+ * @remarks
1310
+ * This actions collection is created during configuration initialisation
1311
+ * and combines inherited actions with local action definitions.
1312
+ *
1313
+ * Action assembly workflow:
1314
+ *
1315
+ * <ol>
1316
+ * <li>Undefined until <code>BuildConfiguration.initialise()</code> is
1317
+ * called.</li>
1318
+ * <li>Collect actions from all inherited configurations in the inheritance
1319
+ * chain.</li>
1320
+ * <li>Create new <code>Actions</code> collection with inherited
1321
+ * actions map and local action definitions.</li>
1322
+ * <li>Actions inherit the configuration's substitution variables context,
1323
+ * including properties and matrix parameters.</li>
1324
+ * </ol>
1325
+ *
1326
+ * Actions are accessible after configuration initialisation but remain
1327
+ * themselves uninitialised until retrieved and initialised individually,
1328
+ * maintaining the lazy evaluation pattern.
1329
+ */
1330
+ protected _actions: Actions | undefined
1331
+
1332
+ /**
1333
+ * The resolved build folder relative path.
1334
+ *
1335
+ * @remarks
1336
+ * This path specifies where build outputs for this configuration should be
1337
+ * placed, computed during initialisation and added back to properties for
1338
+ * use in subsequent substitutions.
1339
+ *
1340
+ * Computation workflow:
1341
+ *
1342
+ * <ol>
1343
+ * <li>Undefined until <code>BuildConfiguration.initialise()</code> is
1344
+ * called.</li>
1345
+ * <li>Not computed for hidden configurations (optimization).</li>
1346
+ * <li>If <code>buildFolderRelativePath</code> property exists, perform Liquid
1347
+ * substitution with full configuration context.</li>
1348
+ * <li>Otherwise, generate default path:
1349
+ * <code>build/\{sanitized-config-name\}</code>.</li>
1350
+ * <li>Added to <code>properties.buildFolderRelativePath</code> for use
1351
+ * in action
1352
+ * commands and dependency references.</li>
1353
+ * </ol>
1354
+ *
1355
+ * The path is relative to the package root and used by build tools to
1356
+ * organize outputs from different configurations.
1357
+ */
1358
+ protected _buildFolderRelativePath?: string
1359
+
1360
+ /**
1361
+ * Set of inherited configuration names for circular reference detection.
1362
+ *
1363
+ * @remarks
1364
+ * This set tracks the inheritance chain being processed to detect and
1365
+ * prevent circular inheritance references.
1366
+ *
1367
+ * Detection mechanism:
1368
+ *
1369
+ * <ol>
1370
+ * <li>Initially empty when configuration initialisation begins.</li>
1371
+ * <li>Each inherited configuration name is added before processing that
1372
+ * configuration's inheritance.</li>
1373
+ * <li>If a configuration attempts to inherit from a name already in the
1374
+ * set, a circular reference exists.</li>
1375
+ * <li>Circular references trigger <code>InputError</code> with details
1376
+ * about the problematic inheritance chain.</li>
1377
+ * </ol>
1378
+ *
1379
+ * Example: If config A inherits from B, B from C, and C from A, the
1380
+ * circular dependency is detected when C attempts to inherit from A.
1381
+ */
1382
+ protected _inheritedNamesSet: Set<string> = new Set<string>()
1383
+
1384
+ /**
1385
+ * Flag indicating whether the configuration has been initialised.
1386
+ *
1387
+ * @remarks
1388
+ * This flag ensures idempotent initialization and prevents redundant
1389
+ * processing when {@link BuildConfiguration.initialise} is called
1390
+ * multiple times.
1391
+ *
1392
+ * State transitions:
1393
+ *
1394
+ * <ol>
1395
+ * <li>Initially <code>false</code> after construction.</li>
1396
+ * <li>Set to <code>true</code> after successful inheritance resolution,
1397
+ * property
1398
+ * merging, dependency substitution, and action preparation.</li>
1399
+ * <li>Checked at the start of
1400
+ * <code>BuildConfiguration.initialise()</code> to return early if
1401
+ * already initialised.</li>
1402
+ * </ol>
1403
+ *
1404
+ * This pattern is critical for inheritance processing, as configurations
1405
+ * may be initialised multiple times when referenced by multiple children,
1406
+ * but should only process their inheritance chain once.
1407
+ */
1408
+ protected _isInitialised = false
1409
+
1410
+ // --------------------------------------------------------------------------
1411
+ // Constructor and async initialiser.
1412
+
1413
+ /**
1414
+ * Constructs a build configuration instance.
1415
+ *
1416
+ * @param buildConfigurationName - The configuration name after substitution.
1417
+ * @param templateBuildConfigurationName - The template configuration name, if
1418
+ * derived from a template.
1419
+ * @param jsonBuildConfiguration - The JSON configuration definition.
1420
+ * @param parentBuildConfigurations - The parent configurations collection.
1421
+ * @param matrixParameters - Optional matrix parameter values for
1422
+ * template-generated configurations.
1423
+ *
1424
+ * @remarks
1425
+ * The constructor performs partial initialisation. Full initialisation
1426
+ * requires calling {@link BuildConfiguration.initialise}.
1427
+ */
1428
+ constructor({
1429
+ buildConfigurationName,
1430
+ templateBuildConfigurationName,
1431
+ jsonBuildConfiguration,
1432
+ parentBuildConfigurations,
1433
+ matrixParameters,
1434
+ }: BuildConfigurationConstructorParameters) {
1435
+ assert(buildConfigurationName, 'buildConfigurationName is required')
1436
+ assert(jsonBuildConfiguration, 'jsonBuildConfiguration is required')
1437
+ assert(parentBuildConfigurations, 'parentBuildConfigurations is required')
1438
+
1439
+ const log = parentBuildConfigurations.log
1440
+ this._log = log
1441
+
1442
+ log.trace(`${BuildConfiguration.name}(${buildConfigurationName})`)
1443
+
1444
+ this.name = buildConfigurationName
1445
+ this.jsonBuildConfiguration = jsonBuildConfiguration
1446
+ this.parentBuildConfigurations = parentBuildConfigurations
1447
+ if (matrixParameters !== undefined) {
1448
+ this.matrixParameters = matrixParameters
1449
+ }
1450
+ if (templateBuildConfigurationName !== undefined) {
1451
+ this.templateName = templateBuildConfigurationName
1452
+ }
1453
+
1454
+ this._substitutionsVariables = {
1455
+ ...this.parentBuildConfigurations.substitutionsVariables,
1456
+ }
1457
+
1458
+ this.isHidden = this.jsonBuildConfiguration.hidden ?? false
1459
+
1460
+ this.isTemplate = this.templateName !== undefined
1461
+
1462
+ // The rest of the initialisation is done in the async initialiser.
1463
+ }
1464
+
1465
+ /**
1466
+ * Completes the async initialisation of the build configuration.
1467
+ *
1468
+ * @remarks
1469
+ * This method resolves inheritance, applies variable substitutions,
1470
+ * computes dependencies, and prepares actions.
1471
+ *
1472
+ * Initialisation workflow:
1473
+ *
1474
+ * <ol>
1475
+ * <li>Return early if already initialised (idempotent behaviour).</li>
1476
+ * <li>For template configurations: Call
1477
+ * <code>_substituteTemplate()</code> to substitute
1478
+ * matrix parameters throughout the entire JSON structure.</li>
1479
+ * <li>For non-template configurations: Call
1480
+ * <code>_substituteInherits()</code> to substitute
1481
+ * only the inherits field.</li>
1482
+ * <li>Call <code>_processInherits()</code> to:
1483
+ * <ul>
1484
+ * <li>Process inheritance chain recursively with circular reference
1485
+ * detection.</li>
1486
+ * <li>Merge properties, dependencies, and devDependencies from inherited
1487
+ * configurations (later overrides earlier).</li>
1488
+ * <li>Collect inherited actions into a map.</li>
1489
+ * </ul>
1490
+ * </li>
1491
+ * <li>Apply local properties and update substitution variables context.</li>
1492
+ * <li>For visible configurations: Compute build folder relative path via
1493
+ * <code>_getBuildFolderRelativePath()</code>.</li>
1494
+ * <li>Substitute Liquid templates in dependencies and devDependencies.</li>
1495
+ * <li>Create actions collection with inherited actions and local
1496
+ * actions.</li>
1497
+ * </ol>
1498
+ *
1499
+ * The substitution context includes package variables, configuration
1500
+ * properties, matrix parameters (for templates), and the configuration
1501
+ * object itself accessible via `configuration.name`, etc.
1502
+ *
1503
+ * @returns A promise that resolves to `true` if initialisation was performed,
1504
+ * or `false` if already initialised.
1505
+ *
1506
+ * @throws {@link ConfigurationError}
1507
+ * If substitutions fail.
1508
+ *
1509
+ * @throws {@link InputError}
1510
+ * If inheritance references are invalid or circular.
1511
+ */
1512
+ async initialise(): Promise<boolean> {
1513
+ const log = this._log
1514
+ log.trace(`${BuildConfiguration.name}.initialise()` + ` @${this.name}`)
1515
+
1516
+ if (this._isInitialised) {
1517
+ log.trace(
1518
+ `${BuildConfiguration.name}.initialise()` + ` @${this.name} again`
1519
+ )
1520
+ return false
1521
+ }
1522
+
1523
+ log.trace(`${BuildConfiguration.name}.initialise()` + ` @${this.name}`)
1524
+ let localJsonBuildConfiguration: JsonBuildConfigurationContent
1525
+
1526
+ if (this.isTemplate) {
1527
+ localJsonBuildConfiguration = await this._substituteTemplate()
1528
+ } else {
1529
+ localJsonBuildConfiguration = await this._substituteInherits()
1530
+ }
1531
+
1532
+ // Add inherited properties, dependencies, devDependencies, and actions.
1533
+ const inheritedActionsMap = await this._processInherits(
1534
+ localJsonBuildConfiguration
1535
+ )
1536
+
1537
+ this.properties = {
1538
+ ...this.properties,
1539
+ ...localJsonBuildConfiguration.properties,
1540
+ }
1541
+
1542
+ assert(this.name, 'buildConfigurationName missing')
1543
+ this._substitutionsVariables = {
1544
+ ...this.parentBuildConfigurations.substitutionsVariables,
1545
+ properties: {
1546
+ ...this._substitutionsVariables.properties,
1547
+ ...this.properties,
1548
+ },
1549
+ matrix: this.matrixParameters ?? {},
1550
+ configuration: {
1551
+ ...localJsonBuildConfiguration,
1552
+ name: this.name,
1553
+ },
1554
+ }
1555
+
1556
+ if (!this.isHidden) {
1557
+ this._buildFolderRelativePath = await this._getBuildFolderRelativePath()
1558
+
1559
+ // Add the buildFolderRelativePath property.
1560
+ // Note: the async initialiser was needed due to this async operation.
1561
+ const properties = this._substitutionsVariables.properties
1562
+ properties.buildFolderRelativePath = this._buildFolderRelativePath
1563
+ }
1564
+
1565
+ this.dependencies = {
1566
+ ...this.dependencies,
1567
+ ...localJsonBuildConfiguration.dependencies,
1568
+ }
1569
+
1570
+ this.devDependencies = {
1571
+ ...this.devDependencies,
1572
+ ...localJsonBuildConfiguration.devDependencies,
1573
+ }
1574
+
1575
+ const unsubstitutedDependencies = {
1576
+ dependencies: this.dependencies,
1577
+ devDependencies: this.devDependencies,
1578
+ }
1579
+
1580
+ const stringifiedDependencies = JSON.stringify(unsubstitutedDependencies)
1581
+
1582
+ if (hasLiquidSyntax(stringifiedDependencies)) {
1583
+ let substitutedDependencies
1584
+ try {
1585
+ substitutedDependencies = await performSubstitutions({
1586
+ log,
1587
+ engine: this.parentBuildConfigurations.engine,
1588
+ input: stringifiedDependencies,
1589
+ substitutionsVariables: this._substitutionsVariables,
1590
+ })
1591
+ } catch (error) {
1592
+ const message =
1593
+ getErrorMessage(error) +
1594
+ ` in buildConfiguration "${this.name}" dependencies`
1595
+ throw new ConfigurationError(message)
1596
+ }
1597
+ const parsedDependencies = JSON.parse(
1598
+ substitutedDependencies
1599
+ ) as JsonBuildConfigurationContent
1600
+
1601
+ // Safety net: These fallbacks to empty objects handle cases where the
1602
+ // dependencies fields might be undefined after JSON parsing. This is
1603
+ // unlikely because the JSON schema validation ensures these are objects
1604
+ // when present, but provides robustness against malformed configuration
1605
+ // or future schema changes.
1606
+ /* c8 ignore start - safety net, they are always defined */
1607
+ this.dependencies = parsedDependencies.dependencies ?? {}
1608
+ this.devDependencies = parsedDependencies.devDependencies ?? {}
1609
+ /* c8 ignore stop */
1610
+ }
1611
+
1612
+ this._actions = new Actions({
1613
+ log: this._log,
1614
+ engine: this.parentBuildConfigurations.engine,
1615
+ substitutionsVariables: this._substitutionsVariables,
1616
+ inheritedActionsMap,
1617
+ jsonActions: localJsonBuildConfiguration.actions,
1618
+ buildConfiguration: this,
1619
+ })
1620
+
1621
+ log.trace(
1622
+ `${BuildConfiguration.name}.initialise() `,
1623
+ `@{this.buildConfigurationName}`
1624
+ )
1625
+
1626
+ if (!this.isHidden) {
1627
+ log.trace(
1628
+ this.name,
1629
+ 'buildFolderRelativePath =>',
1630
+ this._buildFolderRelativePath
1631
+ )
1632
+ }
1633
+ log.trace(this.name, 'properties => ', this.properties)
1634
+ log.trace(this.name, 'dependencies => ', this.dependencies)
1635
+ log.trace(this.name, 'devDependencies => ', this.devDependencies)
1636
+
1637
+ // Action names are not available at this point.
1638
+ // log.trace(this.buildConfigurationName, 'actions => ',
1639
+ // this._actions.names)
1640
+
1641
+ this._isInitialised = true
1642
+ return true
1643
+ }
1644
+
1645
+ // --------------------------------------------------------------------------
1646
+ // Public Methods.
1647
+
1648
+ /**
1649
+ * Retrieves the actions collection for this build configuration.
1650
+ *
1651
+ * @returns The actions collection.
1652
+ */
1653
+ get actions(): Actions {
1654
+ assert(
1655
+ this._isInitialised,
1656
+ 'BuildConfiguration must be initialised before ' + 'accessing actions'
1657
+ )
1658
+
1659
+ assert(this._actions !== undefined, 'Actions not initialised')
1660
+ return this._actions
1661
+ }
1662
+
1663
+ /**
1664
+ * Retrieves the build folder relative path for this configuration.
1665
+ *
1666
+ * @returns The build folder relative path.
1667
+ */
1668
+ get buildFolderRelativePath(): string {
1669
+ assert(
1670
+ this._isInitialised,
1671
+ 'BuildConfiguration must be initialised before ' +
1672
+ 'accessing buildFolderRelativePath'
1673
+ )
1674
+ assert(
1675
+ this._buildFolderRelativePath !== undefined,
1676
+ 'BuildConfiguration _buildFolderRelativePath not initialised'
1677
+ )
1678
+ return this._buildFolderRelativePath
1679
+ }
1680
+
1681
+ // --------------------------------------------------------------------------
1682
+ // Private Methods.
1683
+
1684
+ /**
1685
+ * Performs template substitution on the entire build configuration JSON.
1686
+ *
1687
+ * @remarks
1688
+ * This method is invoked during initialisation for template-generated
1689
+ * configurations to substitute matrix parameters throughout the entire
1690
+ * configuration definition.
1691
+ *
1692
+ * Processing steps:
1693
+ *
1694
+ * <ol>
1695
+ * <li>Stringify the entire JSON build configuration object.</li>
1696
+ * <li>Check if the stringified JSON contains template syntax
1697
+ * (<code>\{\{</code> or <code>\{%</code>).</li>
1698
+ * <li>If templates are found:
1699
+ * <ul>
1700
+ * <li>Perform Liquid substitutions with complete variable context
1701
+ * including matrix parameters.</li>
1702
+ * <li>Parse the substituted JSON string back into an object.</li>
1703
+ * </ul>
1704
+ * </li>
1705
+ * <li>If no templates are found, return the original configuration
1706
+ * as-is.</li>
1707
+ * </ol>
1708
+ *
1709
+ * This comprehensive substitution approach ensures matrix parameters can
1710
+ * be referenced anywhere within the configuration (properties, dependencies,
1711
+ * actions, etc.), which is necessary for template-generated configurations
1712
+ * but would be unnecessarily expensive for regular configurations.
1713
+ *
1714
+ * @returns A promise that resolves to the build configuration content with
1715
+ * all template variables substituted.
1716
+ *
1717
+ * @throws {@link ConfigurationError}
1718
+ * If Liquid template substitution fails.
1719
+ */
1720
+ // eslint-disable-next-line max-len
1721
+ protected async _substituteTemplate(): Promise<JsonBuildConfigurationContent> {
1722
+ const log = this._log
1723
+
1724
+ // For templates, perform substitutions on the entire build
1725
+ // configuration JSON, since there can be matrix references everywhere.
1726
+
1727
+ let localJsonBuildConfiguration: JsonBuildConfigurationContent
1728
+
1729
+ const stringifiedJsonBuildConfiguration = JSON.stringify(
1730
+ this.jsonBuildConfiguration
1731
+ )
1732
+ if (hasLiquidSyntax(stringifiedJsonBuildConfiguration)) {
1733
+ let substitutedJsonBuildConfiguration
1734
+ try {
1735
+ substitutedJsonBuildConfiguration = await performSubstitutions({
1736
+ log,
1737
+ engine: this.parentBuildConfigurations.engine,
1738
+ input: stringifiedJsonBuildConfiguration,
1739
+ substitutionsVariables: {
1740
+ ...this._substitutionsVariables,
1741
+ // Safety net: This fallback ensures matrix is always an object.
1742
+ // matrixParameters should be defined when processing templates with
1743
+ // matrix expansion, but this handles edge cases where
1744
+ // initialisation
1745
+ // order or template logic might reference matrix before it's set.
1746
+ /* c8 ignore start - safety net, they are always defined */
1747
+ matrix: this.matrixParameters ?? {},
1748
+ /* c8 ignore stop */
1749
+ configuration: {
1750
+ ...this.jsonBuildConfiguration,
1751
+ name: this.name,
1752
+ },
1753
+ },
1754
+ })
1755
+ } catch (error) {
1756
+ const message =
1757
+ getErrorMessage(error) + ` in buildConfiguration "${this.name}"`
1758
+ throw new ConfigurationError(message)
1759
+ }
1760
+
1761
+ localJsonBuildConfiguration = JSON.parse(
1762
+ substitutedJsonBuildConfiguration
1763
+ ) as JsonBuildConfigurationContent
1764
+ } else {
1765
+ localJsonBuildConfiguration = this.jsonBuildConfiguration
1766
+ }
1767
+ return localJsonBuildConfiguration
1768
+ }
1769
+
1770
+ /**
1771
+ * Performs selective substitution on the inherits field only.
1772
+ *
1773
+ * @remarks
1774
+ * This method is invoked during initialisation for regular (non-template)
1775
+ * configurations to substitute template variables in the inheritance
1776
+ * specification whilst leaving other fields untouched until later processing.
1777
+ *
1778
+ * Processing steps:
1779
+ *
1780
+ * <ol>
1781
+ * <li>Extract the <code>inherits</code> (or deprecated
1782
+ * <code>inherit</code>) field from the
1783
+ * configuration.</li>
1784
+ * <li>Stringify the inherits field and check for template syntax
1785
+ * (<code>\{\{</code> or <code>\{%</code>).</li>
1786
+ * <li>If templates are found:
1787
+ * <ul>
1788
+ * <li>Perform Liquid substitutions with current variable context.</li>
1789
+ * <li>Parse the substituted JSON string back into an object.</li>
1790
+ * <li>Return a new configuration object with the substituted inherits
1791
+ * field and all other fields unchanged.</li>
1792
+ * </ul>
1793
+ * </li>
1794
+ * <li>If no templates are found, return the original configuration
1795
+ * as-is.</li>
1796
+ * </ol>
1797
+ *
1798
+ * This selective approach is more efficient than full JSON substitution
1799
+ * for regular configurations that do not have matrix parameters. The
1800
+ * remaining fields (properties, dependencies, actions) are processed
1801
+ * during inheritance resolution and dependency substitution phases.
1802
+ *
1803
+ * @returns A promise that resolves to the build configuration content with
1804
+ * the inherits field substituted.
1805
+ *
1806
+ * @throws {@link ConfigurationError}
1807
+ * If Liquid template substitution fails on the inherits field.
1808
+ */
1809
+ // eslint-disable-next-line max-len
1810
+ protected async _substituteInherits(): Promise<JsonBuildConfigurationContent> {
1811
+ const log = this._log
1812
+
1813
+ let localJsonBuildConfiguration: JsonBuildConfigurationContent
1814
+
1815
+ // For non-templates, first perform substitutions on 'inherits' only.
1816
+ // The rest of the entries are collected as-is and processed later.
1817
+ const stringifiedJsonInherits = JSON.stringify(
1818
+ this.jsonBuildConfiguration.inherits ?? {}
1819
+ )
1820
+ if (hasLiquidSyntax(stringifiedJsonInherits)) {
1821
+ let substitutedJsonInherits
1822
+ try {
1823
+ substitutedJsonInherits = await performSubstitutions({
1824
+ log,
1825
+ engine: this.parentBuildConfigurations.engine,
1826
+ input: stringifiedJsonInherits,
1827
+ substitutionsVariables: {
1828
+ ...this._substitutionsVariables,
1829
+ configuration: {
1830
+ ...this.jsonBuildConfiguration,
1831
+ name: this.name,
1832
+ },
1833
+ },
1834
+ })
1835
+ } catch (error) {
1836
+ const message =
1837
+ getErrorMessage(error) +
1838
+ ` in buildConfiguration "${this.name}" inherits`
1839
+ throw new ConfigurationError(message)
1840
+ }
1841
+
1842
+ localJsonBuildConfiguration = {
1843
+ ...this.jsonBuildConfiguration,
1844
+ inherits: JSON.parse(
1845
+ substitutedJsonInherits
1846
+ ) as JsonBuildConfigurationInherits,
1847
+ }
1848
+ } else {
1849
+ localJsonBuildConfiguration = this.jsonBuildConfiguration
1850
+ }
1851
+ return localJsonBuildConfiguration
1852
+ }
1853
+
1854
+ /**
1855
+ * Parses the inherits field from JSON configuration.
1856
+ *
1857
+ * @remarks
1858
+ * This helper method extracts and normalises inheritance information from
1859
+ * the configuration, supporting both the current <code>inherits</code>
1860
+ * field and the deprecated <code>inherit</code> field. It handles both
1861
+ * string and array formats.
1862
+ *
1863
+ * Processing steps:
1864
+ *
1865
+ * <ol>
1866
+ * <li>Check for <code>inherits</code> field (current standard).</li>
1867
+ * <li>Fall back to <code>inherit</code> field (deprecated).</li>
1868
+ * <li>Convert single strings to single-element arrays.</li>
1869
+ * <li>Join array elements with line breaks and split to handle
1870
+ * multi-line strings.</li>
1871
+ * </ol>
1872
+ *
1873
+ * @param localJsonBuildConfiguration - The JSON configuration content.
1874
+ * @returns Array of inherited configuration names.
1875
+ */
1876
+ private _parseInheritsField(
1877
+ localJsonBuildConfiguration: JsonBuildConfigurationContent
1878
+ ): string[] {
1879
+ let jsonInherits: string[] = []
1880
+ if (isString(localJsonBuildConfiguration.inherits)) {
1881
+ jsonInherits = [localJsonBuildConfiguration.inherits as string]
1882
+ } else if (Array.isArray(localJsonBuildConfiguration.inherits)) {
1883
+ jsonInherits = localJsonBuildConfiguration.inherits as string[]
1884
+ } else if (isString(localJsonBuildConfiguration.inherit)) {
1885
+ jsonInherits = [localJsonBuildConfiguration.inherit as string]
1886
+ } else if (Array.isArray(localJsonBuildConfiguration.inherit)) {
1887
+ jsonInherits = localJsonBuildConfiguration.inherit as string[]
1888
+ }
1889
+
1890
+ if (jsonInherits.length > 0) {
1891
+ const joinedInherits = jsonInherits.join(os.EOL)
1892
+ return joinedInherits.split(os.EOL)
1893
+ }
1894
+ return jsonInherits
1895
+ }
1896
+
1897
+ /**
1898
+ * Processes and merges a single inherited configuration.
1899
+ *
1900
+ * @remarks
1901
+ * This helper method handles the initialisation of a single inherited
1902
+ * configuration and merges its properties, dependencies, and actions into
1903
+ * the current configuration.
1904
+ *
1905
+ * Processing steps:
1906
+ *
1907
+ * <ol>
1908
+ * <li>Detect circular references by checking the inherited names set.</li>
1909
+ * <li>Initialise the inherited configuration recursively.</li>
1910
+ * <li>Merge properties, dependencies, and devDependencies using spread
1911
+ * operator (later values override earlier ones).</li>
1912
+ * <li>Collect inherited actions into the provided map.</li>
1913
+ * </ol>
1914
+ *
1915
+ * @param inheritedBuildConfigurationName - Name of the configuration to
1916
+ * inherit from.
1917
+ * @param inheritedActionsMap - Map to accumulate inherited actions.
1918
+ * @returns A promise that resolves when processing is complete.
1919
+ *
1920
+ * @throws {@link InputError}
1921
+ * If a circular inheritance reference is detected.
1922
+ *
1923
+ * @throws {@link InputError}
1924
+ * If the inherited configuration name does not exist.
1925
+ */
1926
+ private async _processInheritedConfiguration(
1927
+ inheritedBuildConfigurationName: string,
1928
+ inheritedActionsMap: Map<string, Action>
1929
+ ): Promise<void> {
1930
+ if (inheritedBuildConfigurationName.trim() === '') {
1931
+ return
1932
+ }
1933
+
1934
+ if (
1935
+ !this.parentBuildConfigurations.hasJson(inheritedBuildConfigurationName)
1936
+ ) {
1937
+ throw new ConfigurationError(
1938
+ `buildConfiguration "${this.name}" ` +
1939
+ `inherits from missing "${inheritedBuildConfigurationName}"`
1940
+ )
1941
+ }
1942
+
1943
+ if (this._inheritedNamesSet.has(inheritedBuildConfigurationName)) {
1944
+ throw new ConfigurationError(
1945
+ `buildConfiguration "${this.name}" ` +
1946
+ `inherits from circular reference ` +
1947
+ `"${inheritedBuildConfigurationName}"`
1948
+ )
1949
+ }
1950
+
1951
+ this._inheritedNamesSet.add(inheritedBuildConfigurationName)
1952
+
1953
+ const inheritedBuildConfiguration = this.parentBuildConfigurations.get(
1954
+ inheritedBuildConfigurationName
1955
+ )
1956
+
1957
+ await inheritedBuildConfiguration.initialise()
1958
+
1959
+ // Merge properties, dependencies, devDependencies.
1960
+ // Later ones override earlier ones.
1961
+ this.properties = {
1962
+ ...this.properties,
1963
+ ...inheritedBuildConfiguration.properties,
1964
+ }
1965
+
1966
+ this.dependencies = {
1967
+ ...this.dependencies,
1968
+ ...inheritedBuildConfiguration.dependencies,
1969
+ }
1970
+
1971
+ this.devDependencies = {
1972
+ ...this.devDependencies,
1973
+ ...inheritedBuildConfiguration.devDependencies,
1974
+ }
1975
+
1976
+ await inheritedBuildConfiguration.actions.initialise()
1977
+ for (const actionName of inheritedBuildConfiguration.actions.names) {
1978
+ const action = inheritedBuildConfiguration.actions.get(actionName)
1979
+ inheritedActionsMap.set(actionName, action)
1980
+ }
1981
+ }
1982
+
1983
+ /**
1984
+ * Processes inheritance for a build configuration.
1985
+ *
1986
+ * @remarks
1987
+ * This method implements the inheritance resolution mechanism for build
1988
+ * configurations, enabling configurations to share properties, dependencies,
1989
+ * and actions by inheriting from one or more base configurations,
1990
+ * recursively.
1991
+ *
1992
+ * Processing workflow:
1993
+ *
1994
+ * <ol>
1995
+ * <li>Extract inheritance specification from the configuration:
1996
+ * <ul>
1997
+ * <li>Supports both the current <code>inherits</code> field and the
1998
+ * deprecated <code>inherit</code> field for backwards
1999
+ * compatibility.</li>
2000
+ * <li>Handles both string format (single parent) and array format
2001
+ * (multiple parents).</li>
2002
+ * <li>Normalises line-separated names within array elements to support
2003
+ * multi-line specifications.</li>
2004
+ * </ul>
2005
+ * </li>
2006
+ * <li>Process each inherited configuration sequentially:
2007
+ * <ul>
2008
+ * <li>Skip empty names from the inheritance list.</li>
2009
+ * <li>Validate that the inherited configuration exists in the parent
2010
+ * collection.</li>
2011
+ * <li>Detect circular references by checking
2012
+ * <code>_inheritedNamesSet</code>.</li>
2013
+ * <li>Recursively initialise the inherited configuration (which may
2014
+ * itself have inheritance).</li>
2015
+ * </ul>
2016
+ * </li>
2017
+ * <li>Merge inherited content into the current configuration:
2018
+ * <ul>
2019
+ * <li>Properties: Later inherited configurations override earlier ones,
2020
+ * local properties override all inherited.</li>
2021
+ * <li>Dependencies and devDependencies: Same override behaviour as
2022
+ * properties.</li>
2023
+ * <li>Actions: Collected into a map where later definitions override
2024
+ * earlier ones with the same name.</li>
2025
+ * </ul>
2026
+ * </li>
2027
+ * </ol>
2028
+ *
2029
+ * The inheritance chain is processed depth-first, ensuring that transitive
2030
+ * inheritance (A inherits B, B inherits C) is fully resolved before merging
2031
+ * properties. Circular references are detected to prevent infinite recursion.
2032
+ *
2033
+ * @param localJsonBuildConfiguration - The JSON configuration content after
2034
+ * template or inherits field substitution.
2035
+ * @returns A promise that resolves to a map of inherited actions, where
2036
+ * keys are action names and values are action instances from all inherited
2037
+ * configurations.
2038
+ *
2039
+ * @throws {@link InputError}
2040
+ * If an inherited configuration name does not exist in the parent collection.
2041
+ *
2042
+ * @throws {@link InputError}
2043
+ * If a circular inheritance reference is detected.
2044
+ */
2045
+ protected async _processInherits(
2046
+ localJsonBuildConfiguration: JsonBuildConfigurationContent
2047
+ ): Promise<Map<string, Action>> {
2048
+ const log = this._log
2049
+
2050
+ const inheritsNames = this._parseInheritsField(localJsonBuildConfiguration)
2051
+ this.inheritsNames = inheritsNames
2052
+
2053
+ log.trace(this.name, 'inherits from', this.inheritsNames)
2054
+
2055
+ const inheritedActionsMap: Map<string, Action> = new Map<string, Action>()
2056
+
2057
+ for (const inheritedBuildConfigurationName of inheritsNames) {
2058
+ await this._processInheritedConfiguration(
2059
+ inheritedBuildConfigurationName,
2060
+ inheritedActionsMap
2061
+ )
2062
+ }
2063
+
2064
+ return inheritedActionsMap
2065
+ }
2066
+
2067
+ /**
2068
+ * Computes the build folder relative path for this configuration.
2069
+ *
2070
+ * @remarks
2071
+ * This method resolves the build folder relative path property when
2072
+ * provided and uses a default value based on the configuration name
2073
+ * otherwise.
2074
+ *
2075
+ * Resolution strategy:
2076
+ *
2077
+ * <ol>
2078
+ * <li>Check if buildFolderRelativePath property exists in configuration
2079
+ * properties.</li>
2080
+ * <li>If present and non-empty, perform Liquid substitutions with the
2081
+ * full configuration context.</li>
2082
+ * <li>If substitution fails or property is empty/missing, generate a
2083
+ * default path: `build/{filtered-configuration-name}` where the
2084
+ * configuration name is sanitized for filesystem compatibility.</li>
2085
+ * </ol>
2086
+ *
2087
+ * The computed path is added back to the properties as
2088
+ * `buildFolderRelativePath` for use in subsequent substitutions.
2089
+ *
2090
+ * @returns A promise that resolves to the build folder relative path.
2091
+ */
2092
+ protected async _getBuildFolderRelativePath(): Promise<string> {
2093
+ const log = this._log
2094
+
2095
+ let folderPath: string
2096
+ if (
2097
+ buildFolderRelativePathPropertyName in
2098
+ this._substitutionsVariables.properties
2099
+ ) {
2100
+ folderPath = this._substitutionsVariables.properties[
2101
+ buildFolderRelativePathPropertyName
2102
+ ] as string
2103
+ if (folderPath !== '') {
2104
+ try {
2105
+ // log.trace(this.#substitutionsVariables.configuration)
2106
+ const substitutedFolderPath = await performSubstitutions({
2107
+ log,
2108
+ engine: this.parentBuildConfigurations.engine,
2109
+ input: folderPath,
2110
+ substitutionsVariables: this._substitutionsVariables,
2111
+ })
2112
+ return substitutedFolderPath
2113
+ } catch (error) {
2114
+ const message =
2115
+ getErrorMessage(error) + ` in buildConfiguration "${this.name}"`
2116
+ throw new ConfigurationError(message)
2117
+ }
2118
+ }
2119
+ }
2120
+
2121
+ // Provide a default value, based on the name.
2122
+ const defaultFolderPath = path.join('build', filterPath(this.name))
2123
+ return defaultFolderPath
2124
+ }
2125
+ }
2126
+
2127
+ // ----------------------------------------------------------------------------