@xpack/xpm-lib 3.1.3 → 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,765 @@
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 fs from 'node:fs/promises'
16
+ import * as path from 'node:path'
17
+
18
+ // https://www.npmjs.com/package/semver
19
+ import semver from 'semver'
20
+
21
+ // https://www.npmjs.com/package/@xpack/logger
22
+ import { Logger } from '@xpack/logger'
23
+
24
+ // ----------------------------------------------------------------------------
25
+
26
+ import { ConfigurationError, InputError, PrerequisitesError } from './errors.js'
27
+ import { isString } from '../functions/is-something.js'
28
+ import { hasLiquidSyntax } from '../functions/utils.js'
29
+ import {
30
+ JsonBuildConfiguration,
31
+ JsonBuildConfigurationContent,
32
+ JsonBuildConfigurationTemplate,
33
+ JsonPackageSpecifier,
34
+ JsonXpmPackage,
35
+ } from '../types/json.js'
36
+
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Configuration parameters for constructing a package instance.
41
+ *
42
+ * @remarks
43
+ * This interface defines the required configuration for creating an
44
+ * instance of {@link Package}. Both properties are mandatory.
45
+ *
46
+ * The parameters provide the absolute path to the package folder containing
47
+ * (or that will contain) the <code>package.json</code> file, and the logger
48
+ * for diagnostic output during package operations.
49
+ */
50
+ export interface PackageConstructorParameters {
51
+ /**
52
+ * The absolute path to the package folder.
53
+ */
54
+ packageFolderPath: string
55
+
56
+ /**
57
+ * The logger instance for output and diagnostics.
58
+ */
59
+ log: Logger
60
+ }
61
+
62
+ /**
63
+ * Provides access to package metadata and xpm-specific validation.
64
+ *
65
+ * @remarks
66
+ * This class loads and validates `package.json` content, determines
67
+ * package capabilities, and provides helper methods used across <b>xpm</b>
68
+ * workflows.
69
+ *
70
+ * The package abstraction provides a layer over `package.json` processing
71
+ * with progressive validation:
72
+ *
73
+ * <ol>
74
+ * <li><b>Basic file I/O:</b> Read and write <code>package.json</code> with
75
+ * error handling.</li>
76
+ * <li><b>npm validation:</b> Check for valid npm package structure (name,
77
+ * version).</li>
78
+ * <li><b>xpm validation:</b> Verify <code>xpack</code> section presence
79
+ * and structure.</li>
80
+ * <li><b>Binary package validation:</b> Validate binary-specific metadata
81
+ * (executables, binaries, platforms).</li>
82
+ * <li><b>Capability detection:</b> Determine package features (scripts,
83
+ * actions, build configurations).</li>
84
+ * <li><b>Version checking:</b> Validate minimum <b>xpm</b> version
85
+ * requirements.</li>
86
+ * <li><b>Specifier parsing:</b> Extract scope, name, and version from package
87
+ * identifiers.</li>
88
+ * </ol>
89
+ *
90
+ * This hierarchy allows validation to be performed incrementally as needed,
91
+ * avoiding unnecessary checks for packages that don't meet earlier criteria.
92
+ */
93
+ export class Package {
94
+ // --------------------------------------------------------------------------
95
+ // Public Members.
96
+
97
+ /**
98
+ * The absolute path to the package folder.
99
+ *
100
+ * @remarks
101
+ * This path serves as the base folder for all package operations,
102
+ * including reading/writing `package.json` and resolving relative paths.
103
+ *
104
+ * Path requirements:
105
+ *
106
+ * <ol>
107
+ * <li>Must be an absolute path to a folder.</li>
108
+ * <li>Folder should contain (or will contain) a <code>package.json</code>
109
+ * file.</li>
110
+ * <li>Used to construct the path to <code>package.json</code> as
111
+ * <code>\{packageFolderPath\}/package.json</code>.</li>
112
+ * <li>Remains constant throughout the lifecycle of the
113
+ * <code>Package</code> instance.</li>
114
+ * </ol>
115
+ *
116
+ * The path is set during construction and used by all methods that access
117
+ * or modify `package.json`.
118
+ */
119
+ packageFolderPath: string
120
+
121
+ /**
122
+ * The parsed `package.json` content, when available.
123
+ *
124
+ * @remarks
125
+ * This property caches the parsed `package.json` content after successful
126
+ * reading, avoiding repeated file I/O and parsing operations.
127
+ *
128
+ * Lifecycle states:
129
+ *
130
+ * <ol>
131
+ * <li>Initially undefined when the <code>Package</code> instance
132
+ * is created.</li>
133
+ * <li>Populated by <code>Package.readPackageDotJson()</code> upon
134
+ * successful read and parse.</li>
135
+ * <li>Cleared to undefined if parsing fails with
136
+ * <code>withThrow</code> enabled.</li>
137
+ * <li>Used by validation methods (<code>isNpmPackage</code>,
138
+ * <code>isxpm.Package</code>,
139
+ * <code>isBinaryXpmPackage</code>) to check package capabilities.</li>
140
+ * <li>Not automatically updated when <code>package.json</code> is
141
+ * modified externally;
142
+ * call <code>Package.readPackageDotJson()</code> again to refresh.</li>
143
+ * </ol>
144
+ *
145
+ * The cached content improves performance for packages that perform
146
+ * multiple validation checks without file system access overhead.
147
+ */
148
+ jsonPackage?: JsonXpmPackage
149
+
150
+ // --------------------------------------------------------------------------
151
+ // Protected Members.
152
+
153
+ /**
154
+ * The logger instance for output and diagnostics.
155
+ *
156
+ * @remarks
157
+ * This logger provides trace-level diagnostics for package operations,
158
+ * including file I/O, parsing, validation, and version checking.
159
+ *
160
+ * Logging use cases:
161
+ *
162
+ * <ol>
163
+ * <li>Trace package folder path during construction.</li>
164
+ * <li>Log file read errors when investigating missing
165
+ * <code>package.json</code>.</li>
166
+ * <li>Trace JSON parsing errors for debugging invalid
167
+ * <code>package.json</code>.</li>
168
+ * <li>Log version validation details during <code>minimumXpmRequired</code>
169
+ * checks.</li>
170
+ * <li>Trace package specifier parsing for debugging dependency
171
+ * resolution.</li>
172
+ * </ol>
173
+ *
174
+ * The logger enables detailed diagnostics without affecting normal
175
+ * operation, as trace-level output is typically disabled in production.
176
+ */
177
+ protected readonly _log: Logger
178
+
179
+ // --------------------------------------------------------------------------
180
+ // Constructor.
181
+
182
+ /**
183
+ * Constructs a package helper bound to a specific folder.
184
+ *
185
+ * @param packageFolderPath - The absolute path to the package folder.
186
+ * @param log - The logger instance for output and diagnostics.
187
+ *
188
+ * @throws {@link InputError}
189
+ * If packageFolderPath is not provided or is not an absolute path.
190
+ */
191
+ constructor({ packageFolderPath, log }: PackageConstructorParameters) {
192
+ assert(
193
+ packageFolderPath && path.isAbsolute(packageFolderPath),
194
+ `packageFolderPath must be an absolute path, got: ${packageFolderPath}`
195
+ )
196
+
197
+ this._log = log
198
+ this.packageFolderPath = packageFolderPath
199
+
200
+ log.trace(`${Package.name}(${packageFolderPath})`)
201
+ }
202
+
203
+ // --------------------------------------------------------------------------
204
+ // Public Methods.
205
+
206
+ /**
207
+ * Reads and parses `package.json` from the package folder.
208
+ *
209
+ * @remarks
210
+ * This method provides flexible error handling for scenarios where a
211
+ * missing or invalid `package.json` may be expected (e.g., checking whether
212
+ * a folder is a package) versus scenarios where it indicates a critical
213
+ * error (e.g., operating on a known package).
214
+ *
215
+ * When `withThrow` is false, the method returns undefined for missing or
216
+ * invalid files, allowing callers to handle the absence gracefully. When
217
+ * `withThrow` is true, errors are thrown as {@link InputError} for
218
+ * consistent error handling across the application.
219
+ *
220
+ * @param withThrow - Whether to throw on missing or invalid `package.json`.
221
+ * @returns The parsed `package.json` content, or undefined when missing or
222
+ * invalid and `withThrow` is false.
223
+ *
224
+ * @throws {@link InputError}
225
+ * If `package.json` is missing or invalid and `withThrow` is true.
226
+ */
227
+ async readPackageDotJson({
228
+ withThrow = false,
229
+ }: {
230
+ withThrow?: boolean
231
+ } = {}): Promise<JsonXpmPackage | undefined> {
232
+ const jsonFilePath = path.join(this.packageFolderPath, 'package.json')
233
+
234
+ let fileContent: string | Buffer
235
+ try {
236
+ fileContent = await fs.readFile(jsonFilePath)
237
+ } catch (error) {
238
+ if (withThrow) {
239
+ if (error instanceof Error) {
240
+ this._log.trace(error.message)
241
+ }
242
+ throw new InputError(
243
+ `no package.json in folder ‘${this.packageFolderPath}’`
244
+ )
245
+ } else {
246
+ return undefined
247
+ }
248
+ }
249
+
250
+ try {
251
+ this.jsonPackage = JSON.parse(fileContent.toString()) as JsonXpmPackage
252
+ } catch (error) {
253
+ if (withThrow) {
254
+ this.jsonPackage = undefined
255
+ if (error instanceof Error) {
256
+ this._log.trace(error.message)
257
+ }
258
+ throw new InputError(
259
+ `invalid package.json in folder ‘${this.packageFolderPath}’`
260
+ )
261
+ } else {
262
+ return undefined
263
+ }
264
+ }
265
+ return this.jsonPackage
266
+ }
267
+
268
+ /**
269
+ * Writes the provided `package.json` content to disk.
270
+ *
271
+ * @remarks
272
+ * The JSON content is passed explicitly rather than using the cached
273
+ * value.
274
+ *
275
+ * @param jsonPackage - The `package.json` content to write.
276
+ * @returns A promise that resolves when the file has been written.
277
+ */
278
+ async rewritePackageDotJson(jsonPackage: JsonXpmPackage): Promise<void> {
279
+ const log = this._log
280
+
281
+ assert(jsonPackage, 'jsonPackage is required')
282
+ const jsonString = JSON.stringify(jsonPackage, null, 2) + '\n'
283
+
284
+ const jsonFilePath = path.join(this.packageFolderPath, 'package.json')
285
+ log.trace(`write filePath: '${jsonFilePath}'`)
286
+ await fs.writeFile(jsonFilePath, jsonString)
287
+ }
288
+
289
+ /**
290
+ * Determines whether the `package.json` content represents a valid
291
+ * npm package.
292
+ *
293
+ * @returns `true` if the package has a valid name and version, `false`
294
+ * otherwise.
295
+ */
296
+ isNpmPackage(): boolean {
297
+ const jsonPackage = this.jsonPackage
298
+ if (!jsonPackage) {
299
+ return false
300
+ }
301
+
302
+ if (jsonPackage.name === undefined || jsonPackage.version === undefined) {
303
+ return false
304
+ }
305
+ const name = jsonPackage.name.trim()
306
+ if (name.length === 0) {
307
+ return false
308
+ }
309
+ const version = jsonPackage.version.trim()
310
+ if (version.length === 0) {
311
+ return false
312
+ }
313
+
314
+ return true
315
+ }
316
+
317
+ /**
318
+ * Determines whether the package is an <b>xpm</b> package.
319
+ *
320
+ * @returns `true` if the package is a valid npm package with an xpack
321
+ * section, `false` otherwise.
322
+ */
323
+ isXpmPackage(): boolean {
324
+ const jsonPackage = this.jsonPackage
325
+
326
+ if (!this.isNpmPackage()) {
327
+ return false
328
+ }
329
+
330
+ if (jsonPackage?.xpack === undefined) {
331
+ return false
332
+ }
333
+ return true
334
+ }
335
+
336
+ /**
337
+ * Determines whether the package is a binary <b>xpm</b> package.
338
+ *
339
+ * @remarks
340
+ * Binary packages must have both executables and binaries. The
341
+ * presence of one implies the other, so this method validates consistency.
342
+ *
343
+ * Validation rules:
344
+ *
345
+ * <ol>
346
+ * <li>If <code>xpack.executables</code> (or deprecated
347
+ * <code>xpack.bin</code>) exists, then
348
+ * <code>xpack.binaries</code> and <code>xpack.binaries.platforms</code>
349
+ * must also exist.</li>
350
+ * <li>If <code>xpack.binaries</code> exists, then
351
+ * <code>xpack.binaries.platforms</code> and
352
+ * <code>xpack.executables</code> (or deprecated
353
+ * <code>xpack.bin</code>) must also exist.</li>
354
+ * </ol>
355
+ *
356
+ * This bidirectional validation ensures package metadata consistency and
357
+ * catches incomplete binary package configurations early. The check helps
358
+ * prevent runtime errors when attempting to install or use binary packages
359
+ * with missing metadata.
360
+ *
361
+ * @returns `true` if the package defines binaries and executables, `false`
362
+ * otherwise.
363
+ *
364
+ * @throws {@link InputError}
365
+ * If required binary package fields are missing.
366
+ */
367
+ isBinaryXpmPackage() {
368
+ const jsonPackage = this.jsonPackage
369
+
370
+ if (!this.isXpmPackage()) {
371
+ return false
372
+ }
373
+
374
+ // Since Nov. 2024, `executables` is preferred to `bin`.
375
+ if (jsonPackage?.xpack.executables ?? jsonPackage?.xpack.bin) {
376
+ // If it has `executables` or `bin`, it must have `binaries` and
377
+ // `binaries.platforms` too.
378
+ if (!jsonPackage.xpack.binaries) {
379
+ throw new ConfigurationError(
380
+ "doesn't look like a proper binary xpm package, " +
381
+ 'package.json has no "xpack.binaries"'
382
+ )
383
+ }
384
+
385
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
386
+ if (!jsonPackage.xpack.binaries.platforms) {
387
+ throw new ConfigurationError(
388
+ "doesn't look like a proper binary xpm package, " +
389
+ 'package.json has no "xpack.binaries.platforms"'
390
+ )
391
+ }
392
+ return true
393
+ }
394
+ if (jsonPackage?.xpack.binaries) {
395
+ // If it has `binaries`, it must have `binaries.platforms` and
396
+ // `executables` too.
397
+
398
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
399
+ if (!jsonPackage.xpack.binaries.platforms) {
400
+ throw new ConfigurationError(
401
+ "doesn't look like a proper binary xpm package, " +
402
+ 'package.json has no "xpack.binaries.platforms"'
403
+ )
404
+ }
405
+ // if (!(jsonPackage.xpack.executables ?? jsonPackage.xpack.bin)) {
406
+ throw new ConfigurationError(
407
+ "doesn't look like a proper binary xpm package, " +
408
+ 'package.json has no "xpack.executables"'
409
+ )
410
+ //}
411
+ //return true
412
+ }
413
+ return false
414
+ }
415
+
416
+ /**
417
+ * Determines whether the package is a Node module without <b>xpm</b>
418
+ * metadata.
419
+ *
420
+ * @returns `true` if the package is a Node module without <b>xpm</b>
421
+ * metadata, `false` otherwise.
422
+ */
423
+ isNodeModule() {
424
+ const jsonPackage = this.jsonPackage
425
+
426
+ if (!this.isNpmPackage()) {
427
+ return false
428
+ }
429
+
430
+ if (jsonPackage?.xpack) {
431
+ return false
432
+ }
433
+
434
+ return true
435
+ }
436
+
437
+ /**
438
+ * Determines whether the package is a Node module with a binary entry.
439
+ *
440
+ * @returns `true` if the package is a Node module with a bin entry,
441
+ * `false` otherwise.
442
+ */
443
+ isBinaryNodeModule() {
444
+ const jsonPackage = this.jsonPackage
445
+
446
+ if (!this.isNodeModule()) {
447
+ return false
448
+ }
449
+
450
+ if (jsonPackage?.bin === undefined) {
451
+ return false
452
+ }
453
+
454
+ return true
455
+ }
456
+
457
+ /**
458
+ * Determines whether the package defines any npm scripts.
459
+ *
460
+ * @returns `true` if at least one script is defined, `false` otherwise.
461
+ */
462
+ hasNpmScripts(): boolean {
463
+ const jsonPackage = this.jsonPackage
464
+
465
+ if (
466
+ jsonPackage?.scripts !== undefined &&
467
+ Object.keys(jsonPackage.scripts).length > 0
468
+ ) {
469
+ return true
470
+ }
471
+
472
+ return false
473
+ }
474
+
475
+ /**
476
+ * Determines whether the package defines any <b>xpm</b> actions.
477
+ *
478
+ * @remarks
479
+ * This method performs a comprehensive search for action definitions at
480
+ * both the package level and within build configurations, including
481
+ * template-based configurations.
482
+ *
483
+ * Action detection strategy:
484
+ *
485
+ * <ol>
486
+ * <li>Check for package-level actions in <code>xpack.actions</code>.</li>
487
+ * <li>If no package-level actions, iterate through all build
488
+ * configurations.</li>
489
+ * <li>For each configuration, determine if it's a template (name contains
490
+ * Liquid syntax) or a regular configuration.</li>
491
+ * <li>For templates: Check <code>template.actions</code> for action
492
+ * definitions.</li>
493
+ * <li>For regular configurations: Check <code>actions</code> directly.</li>
494
+ * <li>Return true if any actions are found at any level.</li>
495
+ * </ol>
496
+ *
497
+ * This comprehensive check is useful for determining whether <b>xpm</b>
498
+ * action
499
+ * commands should be available or whether the package requires <b>xpm</b> for
500
+ * build automation.
501
+ *
502
+ * @returns `true` if actions are defined directly or within build
503
+ * configurations, `false` otherwise.
504
+ */
505
+ hasXpmActions(): boolean {
506
+ const json = this.jsonPackage
507
+
508
+ try {
509
+ if (
510
+ json?.xpack.actions !== undefined &&
511
+ Object.keys(json.xpack.actions).length > 0
512
+ ) {
513
+ return true
514
+ }
515
+
516
+ if (
517
+ json?.xpack.buildConfigurations !== undefined &&
518
+ Object.keys(json.xpack.buildConfigurations).length > 0
519
+ ) {
520
+ for (const buildConfigurationName of Object.keys(
521
+ json.xpack.buildConfigurations
522
+ )) {
523
+ const buildConfiguration: JsonBuildConfiguration =
524
+ json.xpack.buildConfigurations[buildConfigurationName]
525
+ if (hasLiquidSyntax(buildConfigurationName)) {
526
+ const buildConfigurationTemplate =
527
+ buildConfiguration as JsonBuildConfigurationTemplate
528
+ if (
529
+ buildConfigurationTemplate.template.actions !== undefined &&
530
+ Object.keys(buildConfigurationTemplate.template.actions).length >
531
+ 0
532
+ ) {
533
+ return true
534
+ }
535
+ } else {
536
+ const buildConfigurationContent =
537
+ buildConfiguration as JsonBuildConfigurationContent
538
+ if (
539
+ buildConfigurationContent.actions !== undefined &&
540
+ Object.keys(buildConfigurationContent.actions).length > 0
541
+ ) {
542
+ return true
543
+ }
544
+ }
545
+ }
546
+ }
547
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
548
+ } catch (error) {
549
+ // In case xpack is not an option to get its properties.
550
+ }
551
+
552
+ return false
553
+ }
554
+
555
+ /**
556
+ * Retrieves the minimum required <b>xpm</b> version specified by the package.
557
+ *
558
+ * @returns The minimum required <b>xpm</b> version without pre-release
559
+ * suffixes, or
560
+ * undefined if not specified.
561
+ */
562
+ getMinimumXpmRequired(): string | undefined {
563
+ const log = this._log
564
+ const jsonPackage = this.jsonPackage
565
+
566
+ log.trace(`${Package.name}.getMinimumXpmRequired()`)
567
+
568
+ const version = jsonPackage?.xpack.minimumXpmRequired
569
+ if (version === undefined) {
570
+ return undefined
571
+ }
572
+
573
+ if (!isString(version)) {
574
+ return undefined
575
+ }
576
+
577
+ // Remove the pre-release part.
578
+ return version.replace(/-.*$/, '')
579
+ }
580
+
581
+ /**
582
+ * Validates the minimum required <b>xpm</b> version against the
583
+ * installed CLI.
584
+ *
585
+ * @remarks
586
+ * This method ensures that packages requiring specific <b>xpm</b>
587
+ * features or bug
588
+ * fixes can enforce a minimum version requirement, preventing runtime
589
+ * errors or unexpected behavior with older <b>xpm</b> versions.
590
+ *
591
+ * Validation workflow:
592
+ *
593
+ * <ol>
594
+ * <li>Check if package is an <b>xpm</b> package with
595
+ * <code>minimumXpmRequired</code> set.</li>
596
+ * <li>Clean the required version by removing pre-release suffixes.</li>
597
+ * <li>Load the <b>xpm</b> CLI's <code>package.json</code> from the
598
+ * provided root folder.</li>
599
+ * <li>Extract and clean the installed <b>xpm</b> version.</li>
600
+ * <li>Compare versions using semver to determine if upgrade is needed.</li>
601
+ * <li>Throw <code>PrerequisitesError</code> if installed version is
602
+ * too old.</li>
603
+ * </ol>
604
+ *
605
+ * Pre-release suffixes are stripped from both versions to ensure that
606
+ * pre-release builds satisfy version requirements (e.g., 1.0.0-beta
607
+ * satisfies minimumXpmRequired: 1.0.0).
608
+ *
609
+ * @param xpmRootFolderPath - The folder path to the <b>xpm</b> CLI package.
610
+ * @returns The cleaned minimum required version, or undefined if no check is
611
+ * required.
612
+ *
613
+ * @throws {@link PrerequisitesError}
614
+ * If the installed <b>xpm</b> version is lower than the required minimum.
615
+ */
616
+ async checkMinimumXpmRequired({
617
+ xpmRootFolderPath,
618
+ }: {
619
+ xpmRootFolderPath: string
620
+ }): Promise<string | undefined> {
621
+ const log = this._log
622
+ const jsonPackage = this.jsonPackage
623
+
624
+ log.trace(`${Package.name}.checkMinimumXpmRequired()`)
625
+
626
+ if (!this.isXpmPackage()) {
627
+ // Not in an xpm package.
628
+ return undefined
629
+ }
630
+
631
+ const minimumXpmRequired = this.getMinimumXpmRequired()
632
+ if (!minimumXpmRequired) {
633
+ log.trace('minimumXpmRequired not used, no checks')
634
+ return undefined
635
+ }
636
+
637
+ log.trace(`minimumXpmRequired: ${minimumXpmRequired}`)
638
+
639
+ let jsonXpmCliPackage: JsonXpmPackage | undefined
640
+ try {
641
+ const cliXpmPackage = new Package({
642
+ log,
643
+ packageFolderPath: xpmRootFolderPath,
644
+ })
645
+ jsonXpmCliPackage = await cliXpmPackage.readPackageDotJson({
646
+ withThrow: true,
647
+ })
648
+ } catch (error) {
649
+ if (error instanceof Error) {
650
+ log.trace(error.message)
651
+ // Safety net: This handles non-Error exceptions. Node.js fs operations
652
+ // and the Package class consistently throw Error instances, but this
653
+ // provides defensive handling for unexpected error types that might
654
+ // occur in edge cases or future code changes.
655
+ /* c8 ignore start - safety net, currently all are Errors */
656
+ } else {
657
+ log.trace(error)
658
+ }
659
+ /* c8 ignore stop */
660
+ return undefined
661
+ }
662
+ assert(jsonXpmCliPackage, 'jsonXpmCliPackage is required')
663
+ log.trace(jsonXpmCliPackage.version)
664
+
665
+ if (!jsonXpmCliPackage.version) {
666
+ return undefined
667
+ }
668
+
669
+ // Remove the pre-release part.
670
+ const xpmVersion = semver.clean(
671
+ jsonXpmCliPackage.version.replace(/-.*$/, '')
672
+ )
673
+ if (!xpmVersion) {
674
+ return undefined
675
+ }
676
+ if (semver.lt(xpmVersion, minimumXpmRequired)) {
677
+ assert(jsonPackage?.name, 'jsonPackage.name is required')
678
+ throw new PrerequisitesError(
679
+ `package '${jsonPackage.name}' ` +
680
+ `requires xpm v${minimumXpmRequired} or later, please upgrade`
681
+ )
682
+ }
683
+ // Check passed.
684
+ return minimumXpmRequired
685
+ }
686
+
687
+ /**
688
+ * Parses an npm package specifier into its components.
689
+ *
690
+ * @remarks
691
+ * npm package specifiers can take several forms:
692
+ *
693
+ * <ul>
694
+ * <li><b>Unscoped without version:</b> <code>package-name</code></li>
695
+ * <li><b>Unscoped with version:</b> <code>package-name\@1.2.3</code></li>
696
+ * <li><b>Scoped without version:</b> <code>\@scope/package-name</code></li>
697
+ * <li><b>Scoped with version:</b>
698
+ * <code>\@scope/package-name\@1.2.3</code></li>
699
+ * </ul>
700
+ *
701
+ * Parsing strategy:
702
+ *
703
+ * <ol>
704
+ * <li>If specifier starts with <code>\@</code>, extract scope and handle
705
+ * scoped format.</li>
706
+ * <li>Split on <code>/</code> to separate scope from name\@version.</li>
707
+ * <li>Split the second part on <code>\@</code> to separate name from
708
+ * version.</li>
709
+ * <li>For unscoped packages, split directly on <code>\@</code> to separate
710
+ * name from version.</li>
711
+ * </ol>
712
+ *
713
+ * The parser handles all valid npm package specifier formats and returns
714
+ * structured components for downstream processing. Invalid formats with
715
+ * multiple slashes are rejected.
716
+ *
717
+ * @param npmPackageSpecifier - The npm package specifier to parse.
718
+ * @returns The parsed package specifier components.
719
+ *
720
+ * @throws {@link InputError}
721
+ * If the specifier is not a valid package name format.
722
+ */
723
+ parsePackageSpecifier({
724
+ npmPackageSpecifier,
725
+ }: {
726
+ npmPackageSpecifier: string
727
+ }): JsonPackageSpecifier {
728
+ assert(npmPackageSpecifier, 'npmPackageSpecifier is required')
729
+
730
+ const log = this._log
731
+
732
+ let scope
733
+ let name
734
+ let version
735
+
736
+ if (npmPackageSpecifier.startsWith('@')) {
737
+ const arr = npmPackageSpecifier.split('/')
738
+ if (arr.length > 2) {
739
+ throw new InputError(`'${npmPackageSpecifier}' not a package name`)
740
+ }
741
+ scope = arr[0]
742
+ if (arr.length > 1) {
743
+ const arr2 = arr[1].split('@')
744
+ name = arr2[0]
745
+ if (arr2.length > 1) {
746
+ version = arr2[1]
747
+ }
748
+ }
749
+ } else {
750
+ const arr2 = npmPackageSpecifier.split('@')
751
+ name = arr2[0]
752
+ if (arr2.length > 1) {
753
+ version = arr2[1]
754
+ }
755
+ }
756
+ log.trace(
757
+ `${npmPackageSpecifier} => ` +
758
+ `${scope ?? '?'} ${name ?? '?'} ${version ?? '?'}`
759
+ )
760
+
761
+ return { scope, name, version }
762
+ }
763
+ }
764
+
765
+ // ----------------------------------------------------------------------------