@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,1157 @@
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 os from 'node:os'
16
+
17
+ import { Logger } from '@xpack/logger'
18
+
19
+ // ----------------------------------------------------------------------------
20
+
21
+ import {
22
+ LiquidSubstitutionsVariables,
23
+ LiquidSubstitutionsStrings,
24
+ } from '../data/substitutions-variables.js'
25
+ import {
26
+ isJsonObject,
27
+ isString,
28
+ isJsonArray,
29
+ } from '../functions/is-something.js'
30
+ import { performSubstitutions } from '../functions/perform-substitutions.js'
31
+ import { getErrorMessage, hasLiquidSyntax } from '../functions/utils.js'
32
+ import {
33
+ JsonActionContent,
34
+ JsonActions,
35
+ JsonActionTemplate,
36
+ } from '../types/json.js'
37
+ import { BuildConfiguration } from './build-configurations.js'
38
+ import { ConfigurationError } from './errors.js'
39
+ import { LiquidEngine } from './liquid-engine.js'
40
+ import { TemplateExpander } from './template-expander.js'
41
+
42
+ // ============================================================================
43
+
44
+ /**
45
+ * Configuration parameters for constructing an actions collection instance.
46
+ *
47
+ * @remarks
48
+ * This interface defines the required configuration for creating an
49
+ * instance of {@link Actions}. Most properties are mandatory except for
50
+ * the optional <code>inheritedActionsMap</code> and
51
+ * <code>buildConfiguration</code> parameters.
52
+ *
53
+ * The parameters provide the actions collection with access to the Liquid
54
+ * templating engine, substitution variables hierarchy, action definitions
55
+ * from the package manifest, optional inherited actions from a parent
56
+ * package, optional build configuration context, and the logger for
57
+ * diagnostic output.
58
+ */
59
+ export interface ActionsConstructorParameters {
60
+ /**
61
+ * The Liquid templating engine for variable substitution.
62
+ */
63
+ engine: LiquidEngine
64
+
65
+ /**
66
+ * The variables available for substitution in action definitions.
67
+ */
68
+ substitutionsVariables: LiquidSubstitutionsVariables
69
+
70
+ /**
71
+ * The JSON object containing action definitions, or undefined if there are
72
+ * no actions.
73
+ */
74
+ jsonActions: JsonActions | undefined
75
+
76
+ /**
77
+ * Optional map of actions inherited from a parent package.
78
+ */
79
+ inheritedActionsMap?: Map<string, Action>
80
+
81
+ /**
82
+ * Optional build configuration this actions collection belongs to.
83
+ */
84
+ buildConfiguration?: BuildConfiguration
85
+
86
+ /**
87
+ * The logger instance for output and diagnostics.
88
+ */
89
+ log: Logger
90
+ }
91
+
92
+ /**
93
+ * A collection of <b>xpm</b> actions for a build configuration or
94
+ * the entire package.
95
+ *
96
+ * @remarks
97
+ * This class manages a collection of named actions, each containing one or
98
+ * more commands to be executed. Actions can belong to a package or a build
99
+ * configuration and support template-based definitions with matrix expansion
100
+ * to generate multiple actions from a single template.
101
+ *
102
+ * The collection always exists, even as empty if no actions are defined.
103
+ *
104
+ * Action lifecycle phases:
105
+ *
106
+ * <ol>
107
+ * <li><b>Construction:</b> Basic setup with optional inheritance from parent
108
+ * package.</li>
109
+ * <li><b>Initialisation:</b> Template name expansion without content
110
+ * evaluation.</li>
111
+ * <li><b>Retrieval:</b> On-demand instantiation when accessed via
112
+ * <code>get()</code>.</li>
113
+ * <li><b>Action Initialisation:</b> Liquid template evaluation and
114
+ * substitution.</li>
115
+ * </ol>
116
+ *
117
+ * This multi-phase approach ensures efficient resource usage by deferring
118
+ * expensive operations until actions are actually needed.
119
+ */
120
+ export class Actions {
121
+ // --------------------------------------------------------------------------
122
+ // Public Members.
123
+
124
+ /**
125
+ * The logger instance for output and diagnostics.
126
+ *
127
+ * @remarks
128
+ * This logger is used throughout the lifecycle of actions collection to
129
+ * provide trace-level diagnostics for debugging template expansion, action
130
+ * instantiation, and variable substitution. It enables visibility into the
131
+ * lazy evaluation process without impacting runtime performance when tracing
132
+ * is disabled.
133
+ */
134
+ readonly log: Logger
135
+
136
+ /**
137
+ * The Liquid templating engine for variable substitution.
138
+ *
139
+ * @remarks
140
+ * This engine instance is shared across all actions in the collection and
141
+ * configured with custom filters for platform detection, path manipulation,
142
+ * and xpm-specific operations. It's used during both template action name
143
+ * expansion and later during individual action command substitution,
144
+ * ensuring consistent template processing throughout the action lifecycle.
145
+ */
146
+ readonly engine: LiquidEngine
147
+
148
+ /**
149
+ * The variables available for substitution in action definitions.
150
+ *
151
+ * @remarks
152
+ * This comprehensive variable hierarchy provides context for template
153
+ * evaluation, including package metadata, build configuration properties,
154
+ * environment variables, platform detection, and path utilities.
155
+ *
156
+ * The hierarchy structure:
157
+ *
158
+ * <ol>
159
+ * <li><b>Base variables:</b> <code>env</code>, <code>os</code>,
160
+ * <code>path</code> (always available).</li>
161
+ * <li><b>Package variables:</b> <code>name</code>, <code>version</code>,
162
+ * <code>dependencies</code>,
163
+ * <code>devDependencies</code>.</li>
164
+ * <li><b>Configuration variables:</b> build folder paths, compiler
165
+ * settings.</li>
166
+ * <li><b>Properties:</b> custom key-value pairs from package or
167
+ * configuration.</li>
168
+ * <li><b>Matrix:</b> parameter combinations for template-generated
169
+ * actions (added per action during initialisation).</li>
170
+ * </ol>
171
+ *
172
+ * These variables are accessible in Liquid templates using dot notation
173
+ * (e.g., `{{ package.name }}`,
174
+ * `{{ configuration.buildFolderRelativePath }}`).
175
+ */
176
+ readonly substitutionsVariables: LiquidSubstitutionsVariables
177
+
178
+ /**
179
+ * The JSON object containing action definitions from the package manifest.
180
+ *
181
+ * @remarks
182
+ * This object holds the raw action definitions as they appear in the
183
+ * `package.json` `xpack.actions` section or within a build configuration's
184
+ * actions. Action definitions can be:
185
+ *
186
+ * <ol>
187
+ * <li><b>Simple strings:</b> Single command to execute.</li>
188
+ * <li><b>String arrays:</b> Multiple commands executed sequentially.</li>
189
+ * <li><b>Template objects:</b> With <code>matrix</code> and
190
+ * <code>template</code> properties for
191
+ * generating multiple actions from a single definition.</li>
192
+ * </ol>
193
+ *
194
+ * Template action names (containing `{{` markers) trigger matrix expansion
195
+ * during initialisation, creating concrete actions from the Cartesian
196
+ * product of matrix parameter values.
197
+ */
198
+ readonly jsonActions: JsonActions
199
+
200
+ /**
201
+ * The build configuration this actions collection belongs to, if any.
202
+ *
203
+ * @remarks
204
+ * This optional reference establishes the hierarchical relationship between
205
+ * actions and build configurations, affecting variable substitution scope
206
+ * and action inheritance.
207
+ *
208
+ * When defined:
209
+ *
210
+ * <ol>
211
+ * <li>Actions inherit configuration-specific variables (build folder paths,
212
+ * compiler settings, toolchain properties).</li>
213
+ * <li>Actions belong to a specific configuration namespace rather than the
214
+ * package root.</li>
215
+ * <li>Logging and diagnostics include the configuration name for
216
+ * context.</li>
217
+ * </ol>
218
+ *
219
+ * When `undefined`:
220
+ *
221
+ * <ol>
222
+ * <li>Actions belong to the package root (<code>xpack.actions</code> in
223
+ * <code>package.json</code>).</li>
224
+ * <li>Only package-level and global variables are available for
225
+ * substitution.</li>
226
+ * </ol>
227
+ */
228
+ readonly buildConfiguration: BuildConfiguration | undefined
229
+
230
+ // --------------------------------------------------------------------------
231
+ // Protected Members.
232
+
233
+ /**
234
+ * Map of action names to their corresponding action instances.
235
+ *
236
+ * @remarks
237
+ * This map serves as the primary action registry, populated during
238
+ * collection initialisation with entries for all discovered actions.
239
+ *
240
+ * Key characteristics:
241
+ *
242
+ * <ol>
243
+ * <li>Known only after <code>Actions.initialise()</code>
244
+ * completes.</li>
245
+ * <li>Possibly empty if there are no actions defined.</li>
246
+ * <li>Values can be <code>undefined</code> to indicate an action
247
+ * exists but hasn't
248
+ * been instantiated yet (lazy loading).</li>
249
+ * <li>For template actions, contains one entry per expanded combination,
250
+ * not the original template definition.</li>
251
+ * </ol>
252
+ *
253
+ * Actions transition from `undefined` to instantiated when first accessed
254
+ * via {@link Actions.get}, implementing the lazy evaluation
255
+ * pattern.
256
+ */
257
+ protected readonly _actionsMap: Map<string, Action | undefined> = new Map<
258
+ string,
259
+ Action | undefined
260
+ >()
261
+
262
+ /**
263
+ * Set of all action names for quick lookup.
264
+ *
265
+ * @remarks
266
+ * This set provides O(1) existence checks for action names, enabling
267
+ * efficient validation during template expansion and duplicate detection.
268
+ *
269
+ * Key characteristics:
270
+ *
271
+ * <ol>
272
+ * <li>Known only after <code>Actions.initialise()</code>
273
+ * completes.</li>
274
+ * <li>Contains all action names including those generated from
275
+ * templates.</li>
276
+ * <li>Used to detect duplicate action names that might arise from template
277
+ * expansion conflicts or explicit duplicates in
278
+ * <code>package.json</code>.</li>
279
+ * </ol>
280
+ *
281
+ * This redundant storage (alongside `_actionsMap`) is justified by the
282
+ * performance benefit for name existence checks, especially in packages
283
+ * with many actions.
284
+ */
285
+ protected readonly _namesSet: Set<string> = new Set<string>()
286
+
287
+ /**
288
+ * Map of expanded action names to their original JSON action names.
289
+ *
290
+ * @remarks
291
+ * This reverse mapping enables retrieving the original action definition
292
+ * from `jsonActions` when lazy-loading action instances.
293
+ *
294
+ * Mapping behavior:
295
+ *
296
+ * <ol>
297
+ * <li><b>For regular actions:</b> Maps action name to itself (identity
298
+ * mapping).</li>
299
+ * <li><b>For template actions:</b> Maps each generated action name back to
300
+ * the original template name (e.g.,
301
+ * <code>test-x64</code> → <code>test-\{\{ matrix.arch \}\}</code>).</li>
302
+ * <li>Enables <code>Actions.get()</code> to locate the correct JSON
303
+ * definition when instantiating an action on demand.</li>
304
+ * </ol>
305
+ *
306
+ * This indirection is essential for the lazy evaluation pattern, allowing
307
+ * deferred instantiation while maintaining the connection to original
308
+ * definitions.
309
+ */
310
+ protected readonly _jsonActionsNamesMap: Map<string, string> = new Map<
311
+ string,
312
+ string
313
+ >()
314
+
315
+ /**
316
+ * Flag indicating whether the actions collection has been initialised.
317
+ *
318
+ * @remarks
319
+ * This flag prevents redundant initialisation and ensures idempotent
320
+ * behavior when {@link Actions.initialise} is called multiple
321
+ * times.
322
+ *
323
+ * State transitions:
324
+ *
325
+ * <ol>
326
+ * <li>Initially <code>false</code> after construction.</li>
327
+ * <li>Set to <code>true</code> after successful template expansion and
328
+ * action name
329
+ * registration.</li>
330
+ * <li>Checked at the beginning of <code>Actions.initialise()</code> to
331
+ * return early if already initialised.</li>
332
+ * </ol>
333
+ *
334
+ * This pattern supports safe repeated calls during complex initialisation
335
+ * sequences without duplicating work or corrupting internal state.
336
+ */
337
+ protected _isInitialised = false
338
+
339
+ /**
340
+ * Cached array of all action names in the collection.
341
+ *
342
+ * @remarks
343
+ * This array provides O(1) access to action names without repeatedly
344
+ * creating new arrays from the map keys, improving performance when the
345
+ * names are accessed multiple times.
346
+ *
347
+ * Key characteristics:
348
+ *
349
+ * <ol>
350
+ * <li>Empty initially after construction.</li>
351
+ * <li>Populated during <code>Actions.initialise()</code> after all
352
+ * action names
353
+ * are determined.</li>
354
+ * <li>Contains all action names including those generated from
355
+ * templates.</li>
356
+ * <li>Returned by the <code>names</code> getter for efficient repeated
357
+ * access.</li>
358
+ * </ol>
359
+ *
360
+ * This cached approach avoids the overhead of calling
361
+ * `Array.from(map.keys())` on every access whilst still
362
+ * providing a clean getter interface.
363
+ */
364
+ protected _names: string[] = []
365
+
366
+ // --------------------------------------------------------------------------
367
+ // Constructor and async initialiser.
368
+
369
+ /**
370
+ * Constructs an actions collection instance.
371
+ *
372
+ * @remarks
373
+ * The constructor performs partial initialisation. Complete initialisation
374
+ * requires calling the `Actions.initialise()` method.
375
+ *
376
+ * @param log - The logger instance for output and diagnostics.
377
+ */
378
+ constructor({
379
+ engine,
380
+ substitutionsVariables,
381
+ jsonActions,
382
+ inheritedActionsMap,
383
+ buildConfiguration,
384
+ log,
385
+ }: ActionsConstructorParameters) {
386
+ assert(log, 'log is required')
387
+ assert(engine, 'engine is required')
388
+ assert(substitutionsVariables, 'substitutionsVariables is required')
389
+
390
+ if (buildConfiguration !== undefined) {
391
+ log.trace(`${Actions.name}()` + ` @${buildConfiguration.name}`)
392
+ } else {
393
+ log.trace(`${Actions.name}()`)
394
+ }
395
+
396
+ this.log = log
397
+ this.engine = engine
398
+ this.substitutionsVariables = substitutionsVariables
399
+ this.jsonActions = jsonActions ?? {}
400
+ if (buildConfiguration !== undefined) {
401
+ this.buildConfiguration = buildConfiguration
402
+ }
403
+
404
+ // If there are inherited actions, add them to the map.
405
+ // They might be overridden by the current definitions.
406
+ if (inheritedActionsMap !== undefined) {
407
+ for (const [
408
+ inheritedActionName,
409
+ inheritedAction,
410
+ ] of inheritedActionsMap) {
411
+ // Make copies of the actions, do not alter the inherited ones.
412
+ const action = new Action({
413
+ actionName: inheritedActionName,
414
+ jsonAction: inheritedAction.jsonAction,
415
+ parentActions: this,
416
+ })
417
+ this._actionsMap.set(inheritedActionName, action)
418
+ }
419
+ }
420
+
421
+ // The rest of the initialisation is done in the async initialiser.
422
+ }
423
+
424
+ /**
425
+ * Completes the async initialisation of the actions collection.
426
+ *
427
+ * @remarks
428
+ * This method implements the first step of lazy evaluation. It processes
429
+ * all action definitions by expanding template action names based on matrix
430
+ * parameters, but does not evaluate the action content or perform Liquid
431
+ * substitutions. The actual template evaluation and variable substitution
432
+ * occur later when individual actions are initialised via
433
+ * {@link Action.initialise}, and only for actions that are
434
+ * actually used. This approach avoids unnecessary operations on unused
435
+ * actions. The method also validates that all expanded action names are
436
+ * unique.
437
+ *
438
+ * @returns A promise that resolves to `true` if initialisation was
439
+ * performed, or `false` if already initialised.
440
+ *
441
+ * @throws {@link ConfigurationError}
442
+ * If duplicate action names are detected or if template expansion fails.
443
+ */
444
+ async initialise(): Promise<boolean> {
445
+ const log = this.log
446
+
447
+ if (this._isInitialised) {
448
+ if (this.buildConfiguration !== undefined) {
449
+ log.trace(
450
+ `${Actions.name}.initialise()` +
451
+ ` @${this.buildConfiguration.name} again`
452
+ )
453
+ } else {
454
+ log.trace(`${Actions.name}.initialise() again`)
455
+ }
456
+ return false
457
+ }
458
+
459
+ if (this.buildConfiguration !== undefined) {
460
+ log.trace(
461
+ `${Actions.name}.initialise()` + ` @${this.buildConfiguration.name}`
462
+ )
463
+ } else {
464
+ log.trace(`${Actions.name}.initialise()`)
465
+ }
466
+
467
+ for (const [actionName, jsonAction] of Object.entries(this.jsonActions)) {
468
+ if (hasLiquidSyntax(actionName)) {
469
+ await this._processTemplate({
470
+ actionName,
471
+ jsonActionTemplate: jsonAction as JsonActionTemplate,
472
+ })
473
+ } else {
474
+ if (this._namesSet.has(actionName)) {
475
+ throw new ConfigurationError(
476
+ `action name "${actionName}" already defined`
477
+ )
478
+ } else {
479
+ this._actionsMap.set(actionName, undefined)
480
+ this._jsonActionsNamesMap.set(actionName, actionName)
481
+ this._namesSet.add(actionName)
482
+ }
483
+ }
484
+ }
485
+ const names = Array.from(this._actionsMap.keys())
486
+ this._names = names
487
+
488
+ this.log.trace(`${Actions.name}.initialise() =>`, names)
489
+
490
+ this._isInitialised = true
491
+ return true
492
+ }
493
+
494
+ // --------------------------------------------------------------------------
495
+ // Public Methods.
496
+
497
+ /**
498
+ * The number of actions in the collection.
499
+ *
500
+ * @remarks
501
+ * This value is known only after `initialise()`.
502
+ *
503
+ * This getter provides direct access to the collection size, enabling
504
+ * callers to check for emptiness or iterate with knowledge of the
505
+ * collection's extent.
506
+ *
507
+ * @returns The number of actions in the collection.
508
+ */
509
+ get size(): number {
510
+ assert(
511
+ this._isInitialised,
512
+ 'Actions collection must be initialised before accessing size'
513
+ )
514
+
515
+ return this._actionsMap.size
516
+ }
517
+
518
+ /**
519
+ * Indicates whether the actions collection is empty.
520
+ *
521
+ * @remarks
522
+ * This value is known only after `initialise()`.
523
+ *
524
+ * @returns `true` if there are no actions, `false` otherwise.
525
+ */
526
+ get isEmpty(): boolean {
527
+ assert(
528
+ this._isInitialised,
529
+ 'Actions collection must be initialised before accessing isEmpty'
530
+ )
531
+
532
+ return this._actionsMap.size === 0
533
+ }
534
+
535
+ /**
536
+ * The names of all actions in the collection.
537
+ *
538
+ * @remarks
539
+ * This value is known only after `initialise()`.
540
+ *
541
+ * This getter returns the cached array of action names for efficient
542
+ * repeated access without recreating the array.
543
+ *
544
+ * @returns An array of action names.
545
+ */
546
+ get names(): string[] {
547
+ assert(
548
+ this._isInitialised,
549
+ 'Actions collection must be initialised before accessing names'
550
+ )
551
+ return this._names
552
+ }
553
+
554
+ /**
555
+ * Checks whether an action with the specified name exists.
556
+ *
557
+ * @remarks
558
+ * This value is known only after `initialise()`.
559
+ *
560
+ * @param actionName - The name of the action to check.
561
+ * @returns `true` if the action exists, `false` otherwise.
562
+ */
563
+ has(actionName: string): boolean {
564
+ assert(
565
+ this._isInitialised,
566
+ 'Actions collection must be initialised before accessing has()'
567
+ )
568
+
569
+ return this._actionsMap.has(actionName)
570
+ }
571
+
572
+ /**
573
+ * Retrieves an action by name, creating it if not yet instantiated.
574
+ *
575
+ * @remarks
576
+ * This method implements lazy evaluation to avoid unnecessary operations.
577
+ * Actions are instantiated on demand but remain uninitialised until actually
578
+ * used. The two-step process works as follows:
579
+ *
580
+ * <ol>
581
+ * <li>During collection initialisation
582
+ * (<code>Actions.initialise()</code>),
583
+ * only the matrix of options is evaluated for each template, expanding
584
+ * only the action names without processing their content.</li>
585
+ * <li>Later, when an action is accessed via this method and subsequently
586
+ * initialised (<code>Action.initialise()</code>), the template is
587
+ * fully evaluated and Liquid substitutions are performed on the
588
+ * commands.</li>
589
+ * </ol>
590
+ *
591
+ * This approach ensures that only actions that are actually used incur the
592
+ * cost of template evaluation and variable substitution.
593
+ *
594
+ * @param actionName - The name of the action to retrieve.
595
+ * @returns The action instance.
596
+ *
597
+ * @throws {@link ConfigurationError}
598
+ * If an action with that name does not exist.
599
+ */
600
+ get(actionName: string): Action {
601
+ assert(
602
+ this._isInitialised,
603
+ 'Actions collection must be initialised before accessing get()'
604
+ )
605
+
606
+ const log = this.log
607
+ log.trace(`${Actions.name}.get(${actionName})`)
608
+
609
+ let action = this._actionsMap.get(actionName)
610
+ if (action === undefined) {
611
+ const jsonActionName = this._jsonActionsNamesMap.get(actionName)
612
+ if (jsonActionName === undefined) {
613
+ throw new ConfigurationError(`action "${actionName}" does not exist`)
614
+ }
615
+ // Safety net: This fallback to empty string is defensive programming.
616
+ // The jsonActions[jsonActionName] should always be defined because
617
+ // _jsonActionsNamesMap is populated from the jsonActions keys during
618
+ // initialisation. The ?? '' provides protection against unexpected
619
+ // runtime inconsistencies between the map and the object.
620
+ /* c8 ignore start - safety net, action names are not undefined. */
621
+ const jsonAction: JsonActionContent = (this.jsonActions[jsonActionName] ??
622
+ '') as JsonActionContent
623
+ /* c8 ignore stop */
624
+
625
+ action = new Action({
626
+ actionName,
627
+ jsonAction,
628
+ parentActions: this,
629
+ })
630
+ this._actionsMap.set(actionName, action)
631
+ }
632
+
633
+ return action
634
+ }
635
+
636
+ // --------------------------------------------------------------------------
637
+ // Private Methods.
638
+
639
+ /**
640
+ * Processes a template action by expanding it and registering the generated
641
+ * actions.
642
+ *
643
+ * @remarks
644
+ * This helper method is called during collection initialisation for each
645
+ * action whose name contains template syntax (<code>\{\{</code> markers).
646
+ *
647
+ * Processing steps:
648
+ *
649
+ * <ol>
650
+ * <li>Calls <code>_expandTemplateActions()</code> to generate all action
651
+ * instances from the template's matrix parameters.</li>
652
+ * <li>Validates that each expanded action name is unique and does not
653
+ * conflict with existing actions.</li>
654
+ * <li>Registers each expanded action in the internal maps:
655
+ * <ul>
656
+ * <li><code>_actionsMap</code>: Maps name to action instance.</li>
657
+ * <li><code>_jsonActionsNamesMap</code>: Maps expanded name back to
658
+ * original template name.</li>
659
+ * <li><code>_namesSet</code>: Tracks all registered names for
660
+ * duplicate detection.</li>
661
+ * </ul>
662
+ * </li>
663
+ * </ol>
664
+ *
665
+ * @param actionName - The template action name containing Liquid variables.
666
+ * @param jsonActionTemplate - The JSON template definition containing matrix
667
+ * parameters and an action template.
668
+ * @returns A promise that resolves when processing is complete.
669
+ *
670
+ * @throws {@link ConfigurationError}
671
+ * If duplicate action names are detected during expansion or if template
672
+ * expansion fails.
673
+ */
674
+ protected async _processTemplate({
675
+ actionName,
676
+ jsonActionTemplate,
677
+ }: {
678
+ actionName: string
679
+ jsonActionTemplate: JsonActionTemplate
680
+ }): Promise<void> {
681
+ // Expand template and generate multiple actions.
682
+ try {
683
+ const expandedActionsMap = await this._expandTemplateActions({
684
+ actionName,
685
+ jsonActionTemplate,
686
+ })
687
+ for (const [expandedActionName, expandedAction] of expandedActionsMap) {
688
+ if (this._namesSet.has(expandedActionName)) {
689
+ throw new ConfigurationError(
690
+ `duplicate action name "${expandedActionName}" ` +
691
+ `could not be generated from template.`
692
+ )
693
+ } else {
694
+ this._actionsMap.set(expandedActionName, expandedAction)
695
+ this._jsonActionsNamesMap.set(expandedActionName, actionName)
696
+ this._namesSet.add(expandedActionName)
697
+ }
698
+ }
699
+ } catch (error) {
700
+ const message = getErrorMessage(error) + ` in action "${actionName}"`
701
+ throw new ConfigurationError(message)
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Expands a template action into multiple concrete actions.
707
+ *
708
+ * @remarks
709
+ * This method uses the {@link TemplateExpander} to compute the Cartesian
710
+ * product of all matrix parameter values and creates a separate action for
711
+ * each combination, substituting matrix values into both the action name
712
+ * and command templates.
713
+ *
714
+ * Processing steps:
715
+ *
716
+ * <ol>
717
+ * <li>Validates matrix and template structure.</li>
718
+ * <li>Delegates to <code>TemplateExpander</code> for matrix processing and
719
+ * name expansion.</li>
720
+ * <li>Creates action instances via factory callback for each
721
+ * combination.</li>
722
+ * </ol>
723
+ *
724
+ * Matrix variables are scoped to individual actions and accessible via
725
+ * the `matrix` namespace during action command evaluation.
726
+ *
727
+ * @param actionName - The template action name containing Liquid variables.
728
+ * @param jsonActionTemplate - The JSON action template definition containing
729
+ * matrix parameters and a template.
730
+ * @returns A promise that resolves to a map of expanded action names to
731
+ * their corresponding action instances.
732
+ *
733
+ * @throws {@link ConfigurationError}
734
+ * If the matrix structure is invalid, template format is incorrect, or
735
+ * substitution fails.
736
+ */
737
+ protected async _expandTemplateActions({
738
+ actionName,
739
+ jsonActionTemplate,
740
+ }: {
741
+ actionName: string
742
+ jsonActionTemplate: JsonActionTemplate
743
+ }): Promise<Map<string, Action>> {
744
+ const log = this.log
745
+ log.trace(`${Actions.name}.#expandTemplateActions(${actionName})`)
746
+
747
+ // Validate template structure
748
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
749
+ if (jsonActionTemplate.matrix == undefined) {
750
+ throw new ConfigurationError(`action "${actionName}" has no matrix`)
751
+ }
752
+
753
+ if (!isJsonObject(jsonActionTemplate.matrix)) {
754
+ throw new ConfigurationError(
755
+ `action "${actionName}" matrix is not an object`
756
+ )
757
+ }
758
+
759
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
760
+ if (jsonActionTemplate.template == undefined) {
761
+ throw new ConfigurationError(`action "${actionName}" has no template`)
762
+ }
763
+
764
+ if (
765
+ !isString(jsonActionTemplate.template) &&
766
+ !isJsonArray(jsonActionTemplate.template)
767
+ ) {
768
+ throw new ConfigurationError(
769
+ `action "${actionName}" template is not a string or array`
770
+ )
771
+ }
772
+
773
+ // Use TemplateExpander for matrix processing and expansion
774
+ const expander = new TemplateExpander<JsonActionContent, Action>({
775
+ engine: this.engine,
776
+ substitutionsVariables: this.substitutionsVariables,
777
+ log: this.log,
778
+ })
779
+
780
+ return await expander.expandTemplate({
781
+ templateName: actionName,
782
+ matrix: jsonActionTemplate.matrix,
783
+ templateContent: jsonActionTemplate.template,
784
+ templateType: 'action',
785
+ instanceFactory: (
786
+ expandedName: string,
787
+ combination: Record<string, string>,
788
+ templateContent: JsonActionContent
789
+ ) =>
790
+ new Action({
791
+ actionName: expandedName,
792
+ jsonAction: templateContent,
793
+ parentActions: this,
794
+ matrixParameters: { ...combination },
795
+ }),
796
+ })
797
+ }
798
+ }
799
+
800
+ // ============================================================================
801
+
802
+ /**
803
+ * Configuration parameters for constructing an action instance.
804
+ *
805
+ * @remarks
806
+ * This interface defines the required configuration for creating an
807
+ * instance of {@link Action}. Most properties are mandatory except for
808
+ * the optional <code>matrixParameters</code>, which is only needed for
809
+ * template-generated actions that were created from matrix expansion.
810
+ *
811
+ * The parameters provide the action with its identity (name), command
812
+ * definitions, access to the parent collection for shared resources, and
813
+ * optional matrix parameter values for template-generated actions.
814
+ */
815
+ export interface ActionConstructorParameters {
816
+ /**
817
+ * The name of the action.
818
+ */
819
+ actionName: string
820
+
821
+ /**
822
+ * The JSON definition of the action commands.
823
+ */
824
+ jsonAction: JsonActionContent
825
+
826
+ /**
827
+ * The parent actions collection this action belongs to.
828
+ */
829
+ parentActions: Actions
830
+
831
+ /**
832
+ * Optional matrix parameter values for template-generated actions.
833
+ */
834
+ matrixParameters?: LiquidSubstitutionsStrings
835
+ }
836
+
837
+ /**
838
+ * An individual <b>xpm</b> action containing commands to be executed.
839
+ *
840
+ * @remarks
841
+ * Actions are lazily initialised, with variable substitution performed
842
+ * only when the action is first retrieved and initialised. This allows for
843
+ * efficient handling of large numbers of actions generated
844
+ * from templates.
845
+ *
846
+ * An action can exist in three states:
847
+ *
848
+ * <ol>
849
+ * <li><b>Undefined:</b> Name is known but instance not yet created.</li>
850
+ * <li><b>Instantiated:</b> Object exists but commands not yet evaluated.</li>
851
+ * <li><b>Initialised:</b> Commands fully evaluated with Liquid
852
+ * substitutions.</li>
853
+ * </ol>
854
+ *
855
+ * This design minimizes memory usage and computation for actions that are
856
+ * defined but never executed, which is common when using matrix templates
857
+ * to generate platform-specific or configuration-specific actions.
858
+ */
859
+ export class Action {
860
+ // --------------------------------------------------------------------------
861
+ // Public Members.
862
+
863
+ /**
864
+ * The name of the action.
865
+ *
866
+ * @remarks
867
+ * This is the final, expanded action name used for identification and
868
+ * execution. For template-generated actions, this is the concrete name
869
+ * after matrix substitution (e.g., `test-x64` rather than
870
+ * `test-{{ matrix.arch }}`).
871
+ *
872
+ * The name is used for:
873
+ *
874
+ * <ol>
875
+ * <li>User-facing identification when listing or executing actions.</li>
876
+ * <li>Logging and diagnostic output to track action lifecycle.</li>
877
+ * <li>Creating copies of inherited actions with preserved names.</li>
878
+ * </ol>
879
+ *
880
+ * Names must be unique within the actions collection, enforced during
881
+ * {@link Actions.initialise}.
882
+ */
883
+ readonly name: string
884
+
885
+ /**
886
+ * The JSON definition of the action commands.
887
+ *
888
+ * @remarks
889
+ * This holds the raw command definition as it appears in `package.json`,
890
+ * before variable substitution. The format can be:
891
+ *
892
+ * <ol>
893
+ * <li><b>Simple string:</b> Single command line.</li>
894
+ * <li><b>String array:</b> Multiple commands for sequential execution.</li>
895
+ * </ol>
896
+ *
897
+ * The definition is preserved in its original form to enable:
898
+ *
899
+ * <ol>
900
+ * <li>Creating copies of inherited actions with identical definitions.</li>
901
+ * <li>Deferred template evaluation during
902
+ * <code>Action.initialise()</code>.</li>
903
+ * <li>Re-evaluation if needed with different variable contexts.</li>
904
+ * </ol>
905
+ *
906
+ * This immutable storage ensures actions can be safely copied and
907
+ * initialised multiple times without side effects.
908
+ */
909
+ readonly jsonAction: JsonActionContent
910
+
911
+ /**
912
+ * The parent actions collection this action belongs to.
913
+ *
914
+ * @remarks
915
+ * This reference maintains the hierarchical relationship between individual
916
+ * actions and their containing collection, providing essential context for
917
+ * action initialisation and execution.
918
+ *
919
+ * The parent collection provides access to:
920
+ *
921
+ * <ol>
922
+ * <li>Liquid templating engine for variable substitution.</li>
923
+ * <li>Substitution variables hierarchy (package metadata, configuration,
924
+ * environment, platform detection).</li>
925
+ * <li>Logger instance for diagnostic output.</li>
926
+ * <li>Build configuration context when actions belong to a specific
927
+ * configuration rather than the package root.</li>
928
+ * </ol>
929
+ *
930
+ * This design enables actions to access shared resources without duplicating
931
+ * them, while maintaining proper scoping for template evaluation. During
932
+ * initialisation, the action combines parent-level substitution variables
933
+ * with its own matrix parameters to create a complete context for Liquid
934
+ * template processing.
935
+ */
936
+ readonly parentActions: Actions
937
+
938
+ /**
939
+ * The matrix parameter values for template-generated actions.
940
+ *
941
+ * @remarks
942
+ * For template-generated actions, this object contains the specific matrix
943
+ * parameter values that produced this action instance from the template.
944
+ *
945
+ * Usage pattern:
946
+ *
947
+ * <ol>
948
+ * <li>Undefined for regular (non-template) actions.</li>
949
+ * <li>For template actions, contains key-value pairs from the matrix
950
+ * combination (e.g.,
951
+ * <code>\{ arch: 'x64', platform: 'linux' \}</code>).</li>
952
+ * <li>Merged into substitution variables during
953
+ * <code>Action.initialise()</code>, making values accessible via the
954
+ * <code>matrix</code> namespace in command templates.</li>
955
+ * <li>Enables the same command template to generate different concrete
956
+ * commands for each matrix combination.</li>
957
+ * </ol>
958
+ *
959
+ * Example: A template with `{{ matrix.arch }}` becomes `x64` when this
960
+ * action's matrix parameters include `{ arch: 'x64' }`.
961
+ */
962
+ protected readonly _matrixParameters?: LiquidSubstitutionsStrings
963
+
964
+ /**
965
+ * The array of command strings after variable substitution.
966
+ *
967
+ * @remarks
968
+ * This array contains the fully evaluated command lines ready for
969
+ * execution, with all Liquid template variables substituted.
970
+ *
971
+ * Lifecycle states:
972
+ *
973
+ * <ol>
974
+ * <li>Undefined initially and until <code>Action.initialise()</code>
975
+ * is called.</li>
976
+ * <li>Populated during initialisation by evaluating
977
+ * <code>jsonAction</code> with the
978
+ * Liquid engine and complete variable context.</li>
979
+ * <li>Array-based JSON definitions are joined, substituted, then split back
980
+ * into individual command lines.</li>
981
+ * <li>Each string represents one command line to be executed
982
+ * sequentially.</li>
983
+ * </ol>
984
+ *
985
+ * Attempting to access via the `commands` getter before initialisation
986
+ * will trigger an assertion error, enforcing correct usage order.
987
+ */
988
+ protected _commands?: string[]
989
+
990
+ /**
991
+ * Flag indicating whether the action has been initialised.
992
+ *
993
+ * @remarks
994
+ * This flag ensures idempotent initialization and prevents redundant
995
+ * template evaluation when {@link Action.initialise} is called
996
+ * multiple times.
997
+ *
998
+ * State transitions:
999
+ *
1000
+ * <ol>
1001
+ * <li>Initially <code>false</code> after construction.</li>
1002
+ * <li>Set to <code>true</code> after successful command substitution and
1003
+ * evaluation.</li>
1004
+ * <li>Checked at the start of <code>Action.initialise()</code> to
1005
+ * return early if already initialised.</li>
1006
+ * </ol>
1007
+ *
1008
+ * This pattern allows safe repeated calls during complex initialization
1009
+ * sequences or when actions are accessed multiple times, avoiding the
1010
+ * computational cost of re-evaluating templates unnecessarily.
1011
+ */
1012
+ protected _isInitialised = false
1013
+
1014
+ // --------------------------------------------------------------------------
1015
+ // Constructor and async initialiser.
1016
+
1017
+ /**
1018
+ * Constructs an action instance.
1019
+ *
1020
+ * @remarks
1021
+ * The constructor performs partial initialisation. Variable substitution
1022
+ * requires calling the {@link Action.initialise} method.
1023
+ *
1024
+ * @param actionName - The name of the action.
1025
+ * @param jsonAction - The JSON definition of the action commands.
1026
+ * @param parentActions - The parent actions collection this action belongs
1027
+ * to.
1028
+ * @param matrixParameters - Optional matrix parameter values for
1029
+ * template-generated actions.
1030
+ */
1031
+ constructor({
1032
+ actionName,
1033
+ jsonAction,
1034
+ parentActions,
1035
+ matrixParameters,
1036
+ }: ActionConstructorParameters) {
1037
+ assert(actionName, 'actionName is required')
1038
+ // assert(jsonAction) // Can be an empty string.
1039
+ assert(parentActions, 'parentActions is required')
1040
+
1041
+ const log = parentActions.log
1042
+ log.trace(`${Action.name}(${actionName})`)
1043
+
1044
+ this.name = actionName
1045
+ this.jsonAction = jsonAction
1046
+ this.parentActions = parentActions
1047
+ if (matrixParameters !== undefined) {
1048
+ this._matrixParameters = matrixParameters
1049
+ }
1050
+ }
1051
+
1052
+ /**
1053
+ * Completes the async initialisation of the action.
1054
+ *
1055
+ * @remarks
1056
+ * This method performs variable substitution on the action commands using
1057
+ * the Liquid templating engine and the available substitution variables,
1058
+ * including any matrix parameters for template-generated actions.
1059
+ *
1060
+ * The substitution context includes:
1061
+ *
1062
+ * <ol>
1063
+ * <li>All package-level substitution variables (configuration, package
1064
+ * metadata, platform detection, etc.).</li>
1065
+ * <li>Build configuration variables if this action belongs to a
1066
+ * configuration.</li>
1067
+ * <li>Matrix parameters for template-generated actions, accessible via
1068
+ * the <code>matrix</code> namespace (e.g.,
1069
+ * <code>\{\{ matrix.arch \}\}</code>).</li>
1070
+ * </ol>
1071
+ *
1072
+ * Array-based command definitions are joined with newlines before
1073
+ * substitution, then split back into individual commands after processing.
1074
+ * This allows commands to span multiple array elements while maintaining
1075
+ * clean formatting in the package manifest.
1076
+ *
1077
+ * @returns A promise that resolves to `true` if initialisation was
1078
+ * performed, or `false` if already initialised.
1079
+ *
1080
+ * @throws {@link ConfigurationError}
1081
+ * If command substitution fails.
1082
+ */
1083
+ async initialise(): Promise<boolean> {
1084
+ const log = this.parentActions.log
1085
+
1086
+ if (this._isInitialised) {
1087
+ log.trace(`${Action.name}.initialise(${this.name}) again`)
1088
+
1089
+ return false
1090
+ }
1091
+
1092
+ log.trace(`${Action.name}.initialise(${this.name})`)
1093
+
1094
+ // Silently accept empty or non-existing actions.
1095
+ const jsonAction = this.jsonAction
1096
+ const inputCommands = Array.isArray(jsonAction)
1097
+ ? jsonAction.join(os.EOL)
1098
+ : jsonAction
1099
+
1100
+ let substitutedCommands
1101
+ if (hasLiquidSyntax(inputCommands)) {
1102
+ try {
1103
+ substitutedCommands = await performSubstitutions({
1104
+ input: inputCommands,
1105
+ engine: this.parentActions.engine,
1106
+ substitutionsVariables: {
1107
+ ...this.parentActions.substitutionsVariables,
1108
+ matrix: this._matrixParameters ?? {},
1109
+ },
1110
+ log,
1111
+ })
1112
+ } catch (error) {
1113
+ const message =
1114
+ getErrorMessage(error) +
1115
+ ` in action "${this.name}" commands substitution`
1116
+ throw new ConfigurationError(message)
1117
+ }
1118
+ } else {
1119
+ substitutedCommands = inputCommands
1120
+ }
1121
+
1122
+ this._commands = substitutedCommands
1123
+ .replace(new RegExp(os.EOL + '$'), '')
1124
+ .split(os.EOL)
1125
+
1126
+ log.trace(`${Action.name}.initialise() =>`, this.name)
1127
+ log.trace(this.name, 'commands =>', this._commands)
1128
+
1129
+ this._isInitialised = true
1130
+ return true
1131
+ }
1132
+
1133
+ // --------------------------------------------------------------------------
1134
+ // Public Methods.
1135
+
1136
+ /**
1137
+ * Retrieves the array of command strings for this action.
1138
+ *
1139
+ * @remarks
1140
+ * The action must be initialised via {@link Action.initialise}
1141
+ * before accessing this property. Attempting to access commands from an
1142
+ * uninitialised action will result in an assertion error.
1143
+ *
1144
+ * @returns The array of command strings after variable substitution.
1145
+ */
1146
+ get commands(): string[] {
1147
+ assert(
1148
+ this._isInitialised,
1149
+ 'Action must be initialised before accessing commands'
1150
+ )
1151
+
1152
+ assert(this._commands, 'Action _commands not initialised')
1153
+ return this._commands
1154
+ }
1155
+ }
1156
+
1157
+ // ----------------------------------------------------------------------------