@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.
- package/README.md +16 -212
- package/dist/classes/actions.d.ts +58 -0
- package/dist/classes/actions.d.ts.map +1 -0
- package/dist/classes/actions.js +250 -0
- package/dist/classes/actions.js.map +1 -0
- package/dist/classes/build-configurations.d.ts +78 -0
- package/dist/classes/build-configurations.d.ts.map +1 -0
- package/dist/classes/build-configurations.js +489 -0
- package/dist/classes/build-configurations.js.map +1 -0
- package/dist/classes/combinations-generator.d.ts +19 -0
- package/dist/classes/combinations-generator.d.ts.map +1 -0
- package/dist/classes/combinations-generator.js +48 -0
- package/dist/classes/combinations-generator.js.map +1 -0
- package/dist/classes/data-model.d.ts +21 -0
- package/dist/classes/data-model.d.ts.map +1 -0
- package/dist/classes/data-model.js +47 -0
- package/dist/classes/data-model.js.map +1 -0
- package/dist/classes/errors.d.ts +13 -0
- package/dist/classes/errors.d.ts.map +1 -0
- package/dist/classes/errors.js +13 -0
- package/dist/classes/errors.js.map +1 -0
- package/dist/classes/init-template-base.d.ts +47 -0
- package/dist/classes/init-template-base.d.ts.map +1 -0
- package/dist/classes/init-template-base.js +358 -0
- package/dist/classes/init-template-base.js.map +1 -0
- package/dist/classes/liquid-drop.d.ts +28 -0
- package/dist/classes/liquid-drop.d.ts.map +1 -0
- package/dist/classes/liquid-drop.js +70 -0
- package/dist/classes/liquid-drop.js.map +1 -0
- package/dist/classes/liquid-engine.d.ts +7 -0
- package/dist/classes/liquid-engine.d.ts.map +1 -0
- package/dist/classes/liquid-engine.js +72 -0
- package/dist/classes/liquid-engine.js.map +1 -0
- package/dist/classes/package.d.ts +31 -0
- package/dist/classes/package.d.ts.map +1 -0
- package/dist/classes/package.js +268 -0
- package/dist/classes/package.js.map +1 -0
- package/dist/classes/platform-detector.d.ts +14 -0
- package/dist/classes/platform-detector.d.ts.map +1 -0
- package/dist/classes/platform-detector.js +26 -0
- package/dist/classes/platform-detector.js.map +1 -0
- package/dist/classes/policies.d.ts +14 -0
- package/dist/classes/policies.d.ts.map +1 -0
- package/dist/classes/policies.js +20 -0
- package/dist/classes/policies.js.map +1 -0
- package/dist/classes/template-expander.d.ts +29 -0
- package/dist/classes/template-expander.d.ts.map +1 -0
- package/dist/classes/template-expander.js +62 -0
- package/dist/classes/template-expander.js.map +1 -0
- package/dist/data/substitutions-variables.d.ts +43 -0
- package/dist/data/substitutions-variables.d.ts.map +1 -0
- package/dist/{lib → data}/substitutions-variables.js +1 -16
- package/dist/data/substitutions-variables.js.map +1 -0
- package/dist/functions/chmod-recursively.d.ts +9 -0
- package/dist/functions/chmod-recursively.d.ts.map +1 -0
- package/dist/functions/chmod-recursively.js +66 -0
- package/dist/functions/chmod-recursively.js.map +1 -0
- package/dist/functions/filter-paths.d.ts +5 -0
- package/dist/functions/filter-paths.d.ts.map +1 -0
- package/dist/functions/filter-paths.js +16 -0
- package/dist/functions/filter-paths.js.map +1 -0
- package/dist/functions/is-something.d.ts +9 -0
- package/dist/functions/is-something.d.ts.map +1 -0
- package/dist/functions/is-something.js +25 -0
- package/dist/functions/is-something.js.map +1 -0
- package/dist/functions/matrix-expander.d.ts +17 -0
- package/dist/functions/matrix-expander.d.ts.map +1 -0
- package/dist/functions/matrix-expander.js +52 -0
- package/dist/functions/matrix-expander.js.map +1 -0
- package/dist/functions/perform-substitutions.d.ts +12 -0
- package/dist/functions/perform-substitutions.d.ts.map +1 -0
- package/dist/functions/perform-substitutions.js +76 -0
- package/dist/functions/perform-substitutions.js.map +1 -0
- package/dist/functions/utils.d.ts +8 -0
- package/dist/functions/utils.d.ts.map +1 -0
- package/dist/functions/utils.js +16 -0
- package/dist/functions/utils.js.map +1 -0
- package/dist/index.d.ts +22 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -29
- package/dist/index.js.map +1 -1
- package/dist/{lib/types.d.ts → types/json.d.ts} +31 -22
- package/dist/types/json.d.ts.map +1 -0
- package/dist/types/json.js +2 -0
- package/dist/types/json.js.map +1 -0
- package/dist/types/xpm-init-template.d.ts +21 -0
- package/dist/types/xpm-init-template.d.ts.map +1 -0
- package/dist/types/xpm-init-template.js +2 -0
- package/dist/types/xpm-init-template.js.map +1 -0
- package/dist/types/xpm.d.ts +16 -0
- package/dist/types/xpm.d.ts.map +1 -0
- package/dist/types/xpm.js +2 -0
- package/dist/types/xpm.js.map +1 -0
- package/package.json +53 -44
- package/src/CODE-REVIEW.md +2167 -0
- package/src/README.md +393 -6
- package/src/classes/actions.ts +1157 -0
- package/src/classes/build-configurations.ts +2127 -0
- package/src/classes/combinations-generator.ts +331 -0
- package/src/classes/data-model.ts +337 -0
- package/src/classes/errors.ts +105 -0
- package/src/classes/init-template-base.ts +1028 -0
- package/src/classes/liquid-drop.ts +376 -0
- package/src/classes/liquid-engine.ts +249 -0
- package/src/classes/package.ts +765 -0
- package/src/classes/platform-detector.ts +237 -0
- package/src/classes/policies.ts +200 -0
- package/src/classes/template-expander.ts +330 -0
- package/src/data/substitutions-variables.ts +390 -0
- package/src/functions/chmod-recursively.ts +195 -0
- package/src/functions/filter-paths.ts +126 -0
- package/src/functions/is-something.ts +223 -0
- package/src/functions/matrix-expander.ts +172 -0
- package/src/functions/perform-substitutions.ts +253 -0
- package/src/functions/utils.ts +151 -0
- package/src/index.ts +72 -19
- package/src/types/json.ts +519 -0
- package/src/types/xpm-init-template.ts +282 -0
- package/src/types/xpm.ts +162 -0
- package/dist/lib/chmod-recursive.d.ts +0 -7
- package/dist/lib/chmod-recursive.d.ts.map +0 -1
- package/dist/lib/chmod-recursive.js +0 -81
- package/dist/lib/chmod-recursive.js.map +0 -1
- package/dist/lib/errors.d.ts +0 -11
- package/dist/lib/errors.d.ts.map +0 -1
- package/dist/lib/errors.js +0 -26
- package/dist/lib/errors.js.map +0 -1
- package/dist/lib/functions/chmod-recursive.d.ts +0 -7
- package/dist/lib/functions/chmod-recursive.d.ts.map +0 -1
- package/dist/lib/functions/chmod-recursive.js +0 -81
- package/dist/lib/functions/chmod-recursive.js.map +0 -1
- package/dist/lib/functions/perform-substitutions.d.ts +0 -20
- package/dist/lib/functions/perform-substitutions.d.ts.map +0 -1
- package/dist/lib/functions/perform-substitutions.js +0 -85
- package/dist/lib/functions/perform-substitutions.js.map +0 -1
- package/dist/lib/functions/utils.d.ts +0 -30
- package/dist/lib/functions/utils.d.ts.map +0 -1
- package/dist/lib/functions/utils.js +0 -70
- package/dist/lib/functions/utils.js.map +0 -1
- package/dist/lib/init-template-base.d.ts +0 -46
- package/dist/lib/init-template-base.d.ts.map +0 -1
- package/dist/lib/init-template-base.js +0 -281
- package/dist/lib/init-template-base.js.map +0 -1
- package/dist/lib/liquid-actions.d.ts +0 -37
- package/dist/lib/liquid-actions.d.ts.map +0 -1
- package/dist/lib/liquid-actions.js +0 -148
- package/dist/lib/liquid-actions.js.map +0 -1
- package/dist/lib/liquid-build-configurations.d.ts +0 -47
- package/dist/lib/liquid-build-configurations.d.ts.map +0 -1
- package/dist/lib/liquid-build-configurations.js +0 -282
- package/dist/lib/liquid-build-configurations.js.map +0 -1
- package/dist/lib/liquid-drop.d.ts +0 -13
- package/dist/lib/liquid-drop.d.ts.map +0 -1
- package/dist/lib/liquid-drop.js +0 -56
- package/dist/lib/liquid-drop.js.map +0 -1
- package/dist/lib/liquid-engine.d.ts +0 -5
- package/dist/lib/liquid-engine.d.ts.map +0 -1
- package/dist/lib/liquid-engine.js +0 -85
- package/dist/lib/liquid-engine.js.map +0 -1
- package/dist/lib/liquid-package.d.ts +0 -17
- package/dist/lib/liquid-package.d.ts.map +0 -1
- package/dist/lib/liquid-package.js +0 -70
- package/dist/lib/liquid-package.js.map +0 -1
- package/dist/lib/package.d.ts +0 -66
- package/dist/lib/package.d.ts.map +0 -1
- package/dist/lib/package.js +0 -700
- package/dist/lib/package.js.map +0 -1
- package/dist/lib/perform-substitutions.d.ts +0 -20
- package/dist/lib/perform-substitutions.d.ts.map +0 -1
- package/dist/lib/perform-substitutions.js +0 -85
- package/dist/lib/perform-substitutions.js.map +0 -1
- package/dist/lib/policies.d.ts +0 -14
- package/dist/lib/policies.d.ts.map +0 -1
- package/dist/lib/policies.js +0 -33
- package/dist/lib/policies.js.map +0 -1
- package/dist/lib/substitutions-variables.d.ts +0 -117
- package/dist/lib/substitutions-variables.d.ts.map +0 -1
- package/dist/lib/substitutions-variables.js.map +0 -1
- package/dist/lib/types.d.ts.map +0 -1
- package/dist/lib/types.js +0 -13
- package/dist/lib/types.js.map +0 -1
- package/dist/lib/utils.d.ts +0 -30
- package/dist/lib/utils.d.ts.map +0 -1
- package/dist/lib/utils.js +0 -70
- package/dist/lib/utils.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/lib/errors.ts +0 -29
- package/src/lib/functions/chmod-recursive.ts +0 -103
- package/src/lib/functions/perform-substitutions.ts +0 -116
- package/src/lib/functions/utils.ts +0 -88
- package/src/lib/init-template-base.ts +0 -408
- package/src/lib/liquid-actions.ts +0 -223
- package/src/lib/liquid-build-configurations.ts +0 -433
- package/src/lib/liquid-drop.ts +0 -99
- package/src/lib/liquid-engine.ts +0 -135
- package/src/lib/liquid-package.ts +0 -108
- package/src/lib/package.ts +0 -947
- package/src/lib/policies.ts +0 -51
- package/src/lib/substitutions-variables.ts +0 -177
- package/src/lib/types.ts +0 -109
- package/src/package.json +0 -3
- package/src/tsconfig.json +0 -10
|
@@ -0,0 +1,1028 @@
|
|
|
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 util from 'node:util'
|
|
16
|
+
import * as readline from 'node:readline/promises'
|
|
17
|
+
import * as path from 'node:path'
|
|
18
|
+
import * as fs from 'node:fs/promises'
|
|
19
|
+
|
|
20
|
+
// https://www.npmjs.com/package/liquidjs
|
|
21
|
+
import { Liquid } from 'liquidjs'
|
|
22
|
+
|
|
23
|
+
import { Logger } from '@xpack/logger'
|
|
24
|
+
|
|
25
|
+
// ----------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
InitTemplateItemValue,
|
|
29
|
+
InitTemplatePropertiesDefinitions,
|
|
30
|
+
InitTemplateSubstitutionsVariables,
|
|
31
|
+
} from '../types/xpm-init-template.js'
|
|
32
|
+
import { Context } from '../types/xpm.js'
|
|
33
|
+
import {
|
|
34
|
+
isString,
|
|
35
|
+
isObject,
|
|
36
|
+
isBoolean,
|
|
37
|
+
isNumber,
|
|
38
|
+
} from '../functions/is-something.js'
|
|
39
|
+
import {
|
|
40
|
+
JsonSyntaxError,
|
|
41
|
+
InputError,
|
|
42
|
+
OutputError,
|
|
43
|
+
ConfigurationError,
|
|
44
|
+
} from './errors.js'
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configuration parameters for constructing an `xpm init` template.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* This interface defines the required configuration for creating an
|
|
53
|
+
* instance of {@link InitTemplateBase} or its derived classes. All
|
|
54
|
+
* properties are mandatory except for the optional `process` parameter,
|
|
55
|
+
* parameter, which defaults to the global Node.js `process` object when
|
|
56
|
+
* not specified.
|
|
57
|
+
*
|
|
58
|
+
* The parameters provide the template with access to the <b>xpm</b>
|
|
59
|
+
* context, file system paths, property definitions, and the process
|
|
60
|
+
* environment necessary for template operations.
|
|
61
|
+
*/
|
|
62
|
+
export interface InitTemplateConstructorParameters {
|
|
63
|
+
/**
|
|
64
|
+
* The <b>xpm</b> context containing configuration and logging utilities.
|
|
65
|
+
*/
|
|
66
|
+
context: Context
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The absolute path to the module folder.
|
|
70
|
+
*/
|
|
71
|
+
__dirname: string
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The absolute path to the templates folder.
|
|
75
|
+
*/
|
|
76
|
+
templatesPath: string
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Definitions of all properties supported by this template.
|
|
80
|
+
*/
|
|
81
|
+
propertiesDefinitions: InitTemplatePropertiesDefinitions
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The Node.js process object (defaults to the global `process`).
|
|
85
|
+
* Intended for testing purposes to allow mocking of process properties
|
|
86
|
+
* and methods.
|
|
87
|
+
*/
|
|
88
|
+
process?: NodeJS.Process
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Base class for `xpm init` templates.
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
95
|
+
* This abstract class provides the foundation for template-based project
|
|
96
|
+
* initialisation. It handles the complete workflow: property validation,
|
|
97
|
+
* interactive user prompts for missing mandatory values, variable
|
|
98
|
+
* substitution, and file generation using the Liquid templating engine.
|
|
99
|
+
*
|
|
100
|
+
* Template workflow:
|
|
101
|
+
*
|
|
102
|
+
* <ol>
|
|
103
|
+
* <li>Properties are validated against their definitions</li>
|
|
104
|
+
* <li>Missing mandatory properties trigger interactive prompts (if TTY)</li>
|
|
105
|
+
* <li>Substitution variables are prepared from properties</li>
|
|
106
|
+
* <li>The <code>InitTemplateBase.generate()</code> method creates project
|
|
107
|
+
* files</li>
|
|
108
|
+
* </ol>
|
|
109
|
+
*
|
|
110
|
+
* Derived classes must implement {@link InitTemplateBase.generate}
|
|
111
|
+
* to define the specific files and folder structure to create.
|
|
112
|
+
*/
|
|
113
|
+
export abstract class InitTemplateBase {
|
|
114
|
+
// --------------------------------------------------------------------------
|
|
115
|
+
// Public Members.
|
|
116
|
+
|
|
117
|
+
// --------------------------------------------------------------------------
|
|
118
|
+
// Protected Members.
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The <b>xpm</b> context containing configuration and logging utilities.
|
|
122
|
+
*/
|
|
123
|
+
protected readonly _context: Context
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The logger instance for output and diagnostics.
|
|
127
|
+
*/
|
|
128
|
+
protected readonly _log: Logger
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Definitions of all properties supported by this template.
|
|
132
|
+
*/
|
|
133
|
+
protected readonly _propertiesDefinitions: InitTemplatePropertiesDefinitions =
|
|
134
|
+
{}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The absolute path to the module folder.
|
|
138
|
+
*/
|
|
139
|
+
protected readonly __dirname: string
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The absolute path to the templates folder.
|
|
143
|
+
*/
|
|
144
|
+
protected readonly _templatesPath: string
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The Liquid templating engine instance.
|
|
148
|
+
*/
|
|
149
|
+
protected readonly _engine: Liquid
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The variables to be used for template substitutions.
|
|
153
|
+
*/
|
|
154
|
+
protected _substitutionsVariables?: InitTemplateSubstitutionsVariables
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Flag indicating whether the template is running in interactive mode.
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* This flag determines whether the template execution involved user
|
|
161
|
+
* interaction through terminal prompts for missing mandatory property
|
|
162
|
+
* values.
|
|
163
|
+
*
|
|
164
|
+
* State management:
|
|
165
|
+
*
|
|
166
|
+
* <ol>
|
|
167
|
+
* <li>Initialised to <code>false</code> upon construction.</li>
|
|
168
|
+
* <li>Set to <code>true</code> in {@link InitTemplateBase.run} if at least
|
|
169
|
+
* one mandatory property was missing and required interactive
|
|
170
|
+
* prompting.</li>
|
|
171
|
+
* <li>Set to <code>false</code> if all mandatory properties were provided
|
|
172
|
+
* via command-line options.</li>
|
|
173
|
+
* </ol>
|
|
174
|
+
*
|
|
175
|
+
* When interactive mode is activated, the context start time is reset
|
|
176
|
+
* after user input to exclude interactive time from performance metrics,
|
|
177
|
+
* ensuring accurate measurement of the template processing duration.
|
|
178
|
+
*/
|
|
179
|
+
protected _isInteractive = false
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The Node.js process object for accessing runtime environment information.
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* This reference provides access to process properties including standard
|
|
186
|
+
* I/O streams, platform information, and architecture details. It is
|
|
187
|
+
* configurable via the constructor to support testing scenarios where
|
|
188
|
+
* process properties need to be mocked or controlled.
|
|
189
|
+
*
|
|
190
|
+
* Usage within the template:
|
|
191
|
+
*
|
|
192
|
+
* <ol>
|
|
193
|
+
* <li>Platform detection via <code>process.platform</code> and
|
|
194
|
+
* <code>process.arch</code> for
|
|
195
|
+
* platform-specific property validation.</li>
|
|
196
|
+
* <li>TTY detection via <code>stdin.isTTY</code> and
|
|
197
|
+
* <code>stdout.isTTY</code> to determine
|
|
198
|
+
* whether interactive prompting is possible.</li>
|
|
199
|
+
* <li>Standard I/O access for interactive user prompts and diagnostic
|
|
200
|
+
* output.</li>
|
|
201
|
+
* </ol>
|
|
202
|
+
*
|
|
203
|
+
* Defaults to the global Node.js <code>process</code> object when not
|
|
204
|
+
* explicitly provided in the constructor, enabling normal runtime
|
|
205
|
+
* behaviour whilst allowing test environments to inject controlled
|
|
206
|
+
* process implementations.
|
|
207
|
+
*/
|
|
208
|
+
protected readonly _process: NodeJS.Process
|
|
209
|
+
|
|
210
|
+
// --------------------------------------------------------------------------
|
|
211
|
+
// Constructor.
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Constructs an `xpm init` template instance.
|
|
215
|
+
*
|
|
216
|
+
* @param context - The <b>xpm</b> context containing configuration and
|
|
217
|
+
* logging.
|
|
218
|
+
* @param __dirname - The absolute path to the module folder.
|
|
219
|
+
* @param templatesPath - The absolute path to the templates folder.
|
|
220
|
+
* @param propertiesDefinitions - The definitions of all supported properties.
|
|
221
|
+
*/
|
|
222
|
+
constructor({
|
|
223
|
+
context,
|
|
224
|
+
__dirname,
|
|
225
|
+
templatesPath,
|
|
226
|
+
propertiesDefinitions,
|
|
227
|
+
process: _process = process,
|
|
228
|
+
}: InitTemplateConstructorParameters) {
|
|
229
|
+
assert(context, 'context is required')
|
|
230
|
+
assert(context.log, 'context.log is required')
|
|
231
|
+
assert(context.config, 'context.context is required')
|
|
232
|
+
assert(context.config.projectName, 'context.config.projectName is required')
|
|
233
|
+
assert(context.config.properties, 'context.config.properties is required')
|
|
234
|
+
assert(__dirname, '__dirname is required')
|
|
235
|
+
assert(templatesPath, 'templatesPath is required')
|
|
236
|
+
assert(propertiesDefinitions, 'propertiesDefinitions is required')
|
|
237
|
+
|
|
238
|
+
this._context = context
|
|
239
|
+
this._log = context.log
|
|
240
|
+
|
|
241
|
+
this._propertiesDefinitions = propertiesDefinitions
|
|
242
|
+
this.__dirname = __dirname
|
|
243
|
+
this._templatesPath = templatesPath
|
|
244
|
+
|
|
245
|
+
this._process = _process
|
|
246
|
+
|
|
247
|
+
this._validatePropertiesDefinitions()
|
|
248
|
+
|
|
249
|
+
// https://liquidjs.com
|
|
250
|
+
this._engine = new Liquid({
|
|
251
|
+
root: this._templatesPath,
|
|
252
|
+
cache: false,
|
|
253
|
+
strictFilters: true, // default: false
|
|
254
|
+
strictVariables: true, // default: false
|
|
255
|
+
trimTagRight: false, // default: false
|
|
256
|
+
trimTagLeft: false, // default: false
|
|
257
|
+
greedy: false,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --------------------------------------------------------------------------
|
|
262
|
+
// Public Methods.
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Executes the template initialisation process.
|
|
266
|
+
*
|
|
267
|
+
* @remarks
|
|
268
|
+
* This method orchestrates the complete template initialisation workflow.
|
|
269
|
+
* It validates all provided properties, determines whether interactive
|
|
270
|
+
* mode is required (when mandatory properties are missing), prompts for
|
|
271
|
+
* missing values if in a TTY environment, prepares substitution variables
|
|
272
|
+
* including the current year, and invokes the template-specific
|
|
273
|
+
* {@link InitTemplateBase.generate} method to create project files.
|
|
274
|
+
*
|
|
275
|
+
* The method automatically applies default values to optional properties
|
|
276
|
+
* that were not explicitly set. In interactive mode, the timer is reset
|
|
277
|
+
* after user input to exclude interactive time from performance metrics.
|
|
278
|
+
*
|
|
279
|
+
* @returns A promise that resolves to 0 on success.
|
|
280
|
+
*
|
|
281
|
+
* @throws {@link JsonSyntaxError}
|
|
282
|
+
* If property validation fails or interactive mode is required but not
|
|
283
|
+
* available (non-TTY environment).
|
|
284
|
+
*/
|
|
285
|
+
async run(): Promise<number> {
|
|
286
|
+
const log = this._log
|
|
287
|
+
log.trace(`${this.constructor.name}.run()`)
|
|
288
|
+
|
|
289
|
+
log.info()
|
|
290
|
+
|
|
291
|
+
const context = this._context
|
|
292
|
+
const config = context.config
|
|
293
|
+
|
|
294
|
+
assert(config.properties, 'config.properties is required')
|
|
295
|
+
|
|
296
|
+
const validationErrors: string[] = []
|
|
297
|
+
for (const [key, val] of Object.entries(config.properties)) {
|
|
298
|
+
try {
|
|
299
|
+
config.properties[key] = this._validatePropertyValue(key, val as string)
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof Error) {
|
|
302
|
+
const errorMessage = `${key}: ${error.message}`
|
|
303
|
+
log.error(errorMessage)
|
|
304
|
+
validationErrors.push(errorMessage)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (validationErrors.length > 0) {
|
|
309
|
+
throw new JsonSyntaxError(
|
|
310
|
+
validationErrors.length === 1
|
|
311
|
+
? '1 invalid property'
|
|
312
|
+
: `${String(validationErrors.length)} invalid properties`
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Properties set by `--property name=value` are in `config.properties`.
|
|
317
|
+
|
|
318
|
+
// If there is at least one mandatory property without an explicit value,
|
|
319
|
+
// enter the interactive mode and ask for the missing values.
|
|
320
|
+
|
|
321
|
+
const mustAsk = Object.keys(this._propertiesDefinitions).some((key) => {
|
|
322
|
+
return (
|
|
323
|
+
this._propertiesDefinitions[key].isMandatory &&
|
|
324
|
+
!config.properties?.[key]
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
let isInteractive
|
|
329
|
+
if (mustAsk) {
|
|
330
|
+
// Need to ask for more values.
|
|
331
|
+
if (!(this._process.stdin.isTTY && this._process.stdout.isTTY)) {
|
|
332
|
+
throw new JsonSyntaxError(
|
|
333
|
+
'Interactive mode not possible without a TTY.'
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await this._askForMoreValues()
|
|
338
|
+
log.trace(util.inspect(config.properties))
|
|
339
|
+
|
|
340
|
+
// Reset start time to skip interactive time.
|
|
341
|
+
context.startTime = Date.now()
|
|
342
|
+
isInteractive = true
|
|
343
|
+
} else {
|
|
344
|
+
// Properties without explicit values get their defaults.
|
|
345
|
+
Object.entries(this._propertiesDefinitions).forEach(([key, val]) => {
|
|
346
|
+
assert(config.properties, 'config.properties is required')
|
|
347
|
+
if (!config.properties[key] && val.default !== undefined) {
|
|
348
|
+
config.properties[key] = val.default
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
isInteractive = false
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this._isInteractive = isInteractive
|
|
355
|
+
|
|
356
|
+
const currentTime = new Date()
|
|
357
|
+
|
|
358
|
+
const substitutionsVariables: InitTemplateSubstitutionsVariables = {
|
|
359
|
+
// Spread all config properties.
|
|
360
|
+
...config.properties,
|
|
361
|
+
// Also pass the properties grouped.
|
|
362
|
+
properties: config.properties,
|
|
363
|
+
propertiesNames: Object.keys(config.properties),
|
|
364
|
+
projectName: config.projectName,
|
|
365
|
+
year: currentTime.getFullYear().toString(),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this._substitutionsVariables = substitutionsVariables
|
|
369
|
+
await this.generate()
|
|
370
|
+
|
|
371
|
+
return 0 // success
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Generates the project files from the template.
|
|
376
|
+
*
|
|
377
|
+
* @remarks
|
|
378
|
+
* This abstract method must be implemented by derived classes to define
|
|
379
|
+
* the specific files and folder structure to create for the project.
|
|
380
|
+
* Implementations should use {@link InitTemplateBase.copyFile},
|
|
381
|
+
* {@link InitTemplateBase.copyFolder}, and
|
|
382
|
+
* {@link InitTemplateBase.render} to create the project structure.
|
|
383
|
+
* The substitution variables are available via the
|
|
384
|
+
* {@link InitTemplateBase._substitutionsVariables} property.
|
|
385
|
+
*
|
|
386
|
+
* The implementation must be <b>asynchronous</b> to allow for file system
|
|
387
|
+
* operations.
|
|
388
|
+
*
|
|
389
|
+
* @returns A promise that resolves when generation is complete.
|
|
390
|
+
*/
|
|
391
|
+
abstract /* async */ generate(): Promise<void>
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Determines whether the current platform is supported.
|
|
395
|
+
*
|
|
396
|
+
* @remarks
|
|
397
|
+
* This method checks platform compatibility using a two-tier matching
|
|
398
|
+
* strategy. First, it looks for an exact match with the current
|
|
399
|
+
* platform-architecture combination (e.g., `darwin-arm64`). If not
|
|
400
|
+
* found, it checks for a platform-only match (e.g., `darwin`). Returns
|
|
401
|
+
* `false` if the platforms array is undefined, empty, or contains no
|
|
402
|
+
* matches for the current execution environment.
|
|
403
|
+
*
|
|
404
|
+
* @param platforms - The array of supported platform identifiers, or
|
|
405
|
+
* undefined if no platforms are specified.
|
|
406
|
+
* @returns `true` if the current platform is supported, `false`
|
|
407
|
+
* otherwise.
|
|
408
|
+
*/
|
|
409
|
+
isPlatformSupported(platforms: string[] | undefined): boolean {
|
|
410
|
+
assert(platforms && platforms.length !== 0, 'platforms array is required')
|
|
411
|
+
|
|
412
|
+
if (platforms.includes(`${this._process.platform}-${this._process.arch}`)) {
|
|
413
|
+
return true
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (platforms.includes(this._process.platform)) {
|
|
417
|
+
return true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Copies a single file from the templates folder to the destination.
|
|
425
|
+
*
|
|
426
|
+
* @remarks
|
|
427
|
+
* This method resolves the source file path relative to the templates
|
|
428
|
+
* folder and copies it to the destination, creating any necessary
|
|
429
|
+
* parent directories. The file is copied without modifications,
|
|
430
|
+
* preserving its content and structure. Use
|
|
431
|
+
* {@link InitTemplateBase.render} instead if variable substitution
|
|
432
|
+
* is needed.
|
|
433
|
+
*
|
|
434
|
+
* @param sourceFileRelativePath - The relative path to the source file
|
|
435
|
+
* within the templates folder.
|
|
436
|
+
* @param destinationFilePath - The destination file path (defaults to
|
|
437
|
+
* the same relative path as the source).
|
|
438
|
+
* @returns A promise that resolves when the file has been copied.
|
|
439
|
+
*/
|
|
440
|
+
async copyFile({
|
|
441
|
+
sourceFileRelativePath,
|
|
442
|
+
destinationFilePath = sourceFileRelativePath,
|
|
443
|
+
}: {
|
|
444
|
+
sourceFileRelativePath: string
|
|
445
|
+
destinationFilePath?: string
|
|
446
|
+
}): Promise<void> {
|
|
447
|
+
const log = this._log
|
|
448
|
+
|
|
449
|
+
await fs.mkdir(path.dirname(destinationFilePath), { recursive: true })
|
|
450
|
+
|
|
451
|
+
const sourceFileAbsolutePath = path.resolve(
|
|
452
|
+
this._templatesPath,
|
|
453
|
+
sourceFileRelativePath
|
|
454
|
+
)
|
|
455
|
+
await fs.copyFile(sourceFileAbsolutePath, destinationFilePath)
|
|
456
|
+
|
|
457
|
+
const destinationFileRelativePath = path.relative(
|
|
458
|
+
this._context.config.cwd,
|
|
459
|
+
destinationFilePath
|
|
460
|
+
)
|
|
461
|
+
log.info(`File '${destinationFileRelativePath}' copied.`)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Copies an entire folder from the templates folder to the destination.
|
|
466
|
+
*
|
|
467
|
+
* @remarks
|
|
468
|
+
* This method recursively copies the complete folder structure,
|
|
469
|
+
* including all files and subfolders, from the source to the
|
|
470
|
+
* destination. The entire folder tree is replicated, preserving the
|
|
471
|
+
* relative paths and structure. Files are copied without
|
|
472
|
+
* modifications; use {@link InitTemplateBase.render} for
|
|
473
|
+
* individual files that require variable substitution.
|
|
474
|
+
*
|
|
475
|
+
* @param sourceFolderRelativePath - The relative path to the source folder
|
|
476
|
+
* within the templates folder.
|
|
477
|
+
* @param destinationFolderPath - The destination folder path (defaults to the
|
|
478
|
+
* same relative path as the source).
|
|
479
|
+
* @returns A promise that resolves when the folder has been copied.
|
|
480
|
+
*/
|
|
481
|
+
async copyFolder({
|
|
482
|
+
sourceFolderRelativePath,
|
|
483
|
+
destinationFolderPath = sourceFolderRelativePath,
|
|
484
|
+
}: {
|
|
485
|
+
sourceFolderRelativePath: string
|
|
486
|
+
destinationFolderPath?: string
|
|
487
|
+
}): Promise<void> {
|
|
488
|
+
const log = this._log
|
|
489
|
+
|
|
490
|
+
const sourceFolderAbsolutePath = path.resolve(
|
|
491
|
+
this._templatesPath,
|
|
492
|
+
sourceFolderRelativePath
|
|
493
|
+
)
|
|
494
|
+
await this._copyFolderRecursively({
|
|
495
|
+
sourceFolderPath: sourceFolderAbsolutePath,
|
|
496
|
+
destinationFolderPath: path.resolve(destinationFolderPath),
|
|
497
|
+
})
|
|
498
|
+
log.info(`Folder '${destinationFolderPath}' copied.`)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Renders a template file using Liquid and writes the output.
|
|
503
|
+
*
|
|
504
|
+
* @remarks
|
|
505
|
+
* This method processes a template file through the Liquid templating
|
|
506
|
+
* engine with the provided substitution variables, generating the final
|
|
507
|
+
* output file. Parent directories are created automatically if they do
|
|
508
|
+
* not exist. The template file should be located in the templates
|
|
509
|
+
* folder and use Liquid syntax for variable references (e.g.,
|
|
510
|
+
* `{{ variableName }}`).
|
|
511
|
+
*
|
|
512
|
+
* The substitution variables include all project properties plus
|
|
513
|
+
* additional context like the current year. If substitutionsVariables
|
|
514
|
+
* is not provided, the instance's substitutionsVariables property is
|
|
515
|
+
* used.
|
|
516
|
+
*
|
|
517
|
+
* @param sourceFilePath - The absolute path to the template
|
|
518
|
+
* file within the templates folder.
|
|
519
|
+
* @param destinationFilePath - The destination path for the rendered
|
|
520
|
+
* file.
|
|
521
|
+
* @param substitutionsVariables - The variables to use for template
|
|
522
|
+
* substitutions (defaults to the instance's substitutionsVariables).
|
|
523
|
+
* @returns A promise that resolves when the file has been rendered and
|
|
524
|
+
* written.
|
|
525
|
+
*
|
|
526
|
+
* @throws {@link OutputError}
|
|
527
|
+
* If template rendering fails.
|
|
528
|
+
*/
|
|
529
|
+
async render({
|
|
530
|
+
sourceFilePath,
|
|
531
|
+
destinationFilePath,
|
|
532
|
+
substitutionsVariables = this._substitutionsVariables,
|
|
533
|
+
}: {
|
|
534
|
+
sourceFilePath: string
|
|
535
|
+
destinationFilePath: string
|
|
536
|
+
substitutionsVariables?: InitTemplateSubstitutionsVariables
|
|
537
|
+
}): Promise<void> {
|
|
538
|
+
assert(
|
|
539
|
+
substitutionsVariables !== undefined,
|
|
540
|
+
'substitutionsVariables is required for rendering templates. ' +
|
|
541
|
+
'Ensure that run() has been called to prepare the variables.'
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
const log = this._log
|
|
545
|
+
const context = this._context
|
|
546
|
+
const config = context.config
|
|
547
|
+
const cwd = config.cwd
|
|
548
|
+
|
|
549
|
+
const sourceFileRelativePath = path.relative(cwd, sourceFilePath)
|
|
550
|
+
const destinationFileRelativePath = path.relative(cwd, destinationFilePath)
|
|
551
|
+
|
|
552
|
+
log.info(
|
|
553
|
+
`Rendering template '${sourceFileRelativePath}' to ` +
|
|
554
|
+
`'${destinationFileRelativePath}'`
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
log.trace(`render(${sourceFilePath}, ${destinationFilePath})`)
|
|
558
|
+
|
|
559
|
+
// const headerPath = path.resolve(codePath, `${pnam}.h`)
|
|
560
|
+
try {
|
|
561
|
+
const fileContent = (await this._engine.renderFile(
|
|
562
|
+
sourceFilePath,
|
|
563
|
+
substitutionsVariables
|
|
564
|
+
)) as string
|
|
565
|
+
|
|
566
|
+
await fs.mkdir(path.dirname(destinationFilePath), { recursive: true })
|
|
567
|
+
await fs.writeFile(destinationFilePath, fileContent, 'utf8')
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (error instanceof Error) {
|
|
570
|
+
throw new OutputError(error.message)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
log.info(`File '${destinationFileRelativePath}' generated.`)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// --------------------------------------------------------------------------
|
|
577
|
+
// Protected Methods.
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Validates a property value against its definition.
|
|
581
|
+
*
|
|
582
|
+
* @remarks
|
|
583
|
+
* This method checks whether the provided value is valid for the
|
|
584
|
+
* specified property according to its type definition. It performs
|
|
585
|
+
* type-specific validation and conversion:
|
|
586
|
+
*
|
|
587
|
+
* <ul>
|
|
588
|
+
* <li><b>For <code>select</code> properties:</b> validates against
|
|
589
|
+
* allowed items andchecks platform compatibility if specified</li>
|
|
590
|
+
* <li><b>For <code>boolean</code> properties:</b> converts
|
|
591
|
+
* <code>'true'</code>/<code>'false'</code>
|
|
592
|
+
* strings to booleans</li>
|
|
593
|
+
* <li><b>For <code>number</code> properties:</b> converts strings
|
|
594
|
+
* to numbers</li>
|
|
595
|
+
* </ul>
|
|
596
|
+
*
|
|
597
|
+
* If the value is empty and a default is defined, the default value is
|
|
598
|
+
* returned. For select properties with platform restrictions, only
|
|
599
|
+
* platform-compatible items are considered valid.
|
|
600
|
+
*
|
|
601
|
+
* @param name - The property name to validate.
|
|
602
|
+
* @param value - The property value to validate.
|
|
603
|
+
* @returns The validated and potentially converted value (string,
|
|
604
|
+
* boolean, or number).
|
|
605
|
+
*
|
|
606
|
+
* @throws {@link ConfigurationError}
|
|
607
|
+
* If the property is unsupported or the value is invalid.
|
|
608
|
+
*/
|
|
609
|
+
protected _validatePropertyValue(
|
|
610
|
+
name: string,
|
|
611
|
+
value: string
|
|
612
|
+
): string | boolean | number {
|
|
613
|
+
const propDef = this._propertiesDefinitions[name]
|
|
614
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
615
|
+
if (propDef === undefined) {
|
|
616
|
+
throw new ConfigurationError(`Unsupported property '${name}'`)
|
|
617
|
+
}
|
|
618
|
+
const trimmedValue = value.trim()
|
|
619
|
+
|
|
620
|
+
if (trimmedValue === '') {
|
|
621
|
+
if (propDef.default !== undefined) {
|
|
622
|
+
return propDef.default
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
switch (propDef.type) {
|
|
626
|
+
case 'select':
|
|
627
|
+
assert(
|
|
628
|
+
propDef.items,
|
|
629
|
+
`Property '${name}' of type 'select' has no items.`
|
|
630
|
+
)
|
|
631
|
+
if (propDef.items[value]) {
|
|
632
|
+
if (typeof propDef.items[value] === 'string') {
|
|
633
|
+
return value
|
|
634
|
+
} else if (
|
|
635
|
+
typeof propDef.items[value] === 'object' &&
|
|
636
|
+
this.isPlatformSupported(propDef.items[value].platforms)
|
|
637
|
+
) {
|
|
638
|
+
return value
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
break
|
|
642
|
+
|
|
643
|
+
case 'boolean':
|
|
644
|
+
if (trimmedValue === 'true') {
|
|
645
|
+
return true
|
|
646
|
+
} else if (trimmedValue === 'false') {
|
|
647
|
+
return false
|
|
648
|
+
}
|
|
649
|
+
break
|
|
650
|
+
|
|
651
|
+
case 'number': {
|
|
652
|
+
const num = Number(trimmedValue)
|
|
653
|
+
if (isFinite(num)) {
|
|
654
|
+
return num
|
|
655
|
+
}
|
|
656
|
+
break
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
case 'string':
|
|
660
|
+
return value
|
|
661
|
+
|
|
662
|
+
// No default, the definition was already validated.
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
throw new ConfigurationError(
|
|
667
|
+
`Unsupported value '${value}' for property '${name}'`
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Prompts the user interactively for missing property values.
|
|
673
|
+
*
|
|
674
|
+
* @remarks
|
|
675
|
+
* This method creates a readline interface and iteratively prompts the
|
|
676
|
+
* user to provide values for properties without explicit values. For
|
|
677
|
+
* each property, the prompt displays:
|
|
678
|
+
*
|
|
679
|
+
* <ul>
|
|
680
|
+
* <li>The property label</li>
|
|
681
|
+
* <li>Valid options (for select and boolean types)</li>
|
|
682
|
+
* <li>The default value in brackets, if available</li>
|
|
683
|
+
* </ul>
|
|
684
|
+
*
|
|
685
|
+
* If the user enters '?', help text is displayed showing the property
|
|
686
|
+
* description and all valid options with their descriptions. Invalid
|
|
687
|
+
* responses are rejected and the prompt is repeated until a valid value
|
|
688
|
+
* is provided. Platform-incompatible options are excluded from select
|
|
689
|
+
* properties.
|
|
690
|
+
*
|
|
691
|
+
* @returns A promise that resolves when all missing values have been
|
|
692
|
+
* collected.
|
|
693
|
+
*/
|
|
694
|
+
protected async _askForMoreValues() {
|
|
695
|
+
const context = this._context
|
|
696
|
+
const config = context.config
|
|
697
|
+
|
|
698
|
+
assert(config.properties, 'config.properties is required')
|
|
699
|
+
|
|
700
|
+
const rl = readline.createInterface({
|
|
701
|
+
input: this._process.stdin,
|
|
702
|
+
output: this._process.stdout,
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
for (const name of Object.keys(this._propertiesDefinitions)) {
|
|
706
|
+
if (config.properties[name]) {
|
|
707
|
+
continue
|
|
708
|
+
}
|
|
709
|
+
const definition = this._propertiesDefinitions[name]
|
|
710
|
+
let prompt = `${definition.label}?`
|
|
711
|
+
switch (definition.type) {
|
|
712
|
+
case 'select': {
|
|
713
|
+
prompt += ' ('
|
|
714
|
+
const validItems = []
|
|
715
|
+
assert(definition.items, 'definition.items is required')
|
|
716
|
+
for (const [ikey, ival] of Object.entries(definition.items)) {
|
|
717
|
+
if (isString(ival)) {
|
|
718
|
+
validItems.push(ikey)
|
|
719
|
+
} else if (
|
|
720
|
+
isObject(ival) &&
|
|
721
|
+
this.isPlatformSupported(
|
|
722
|
+
(ival as InitTemplateItemValue).platforms
|
|
723
|
+
)
|
|
724
|
+
) {
|
|
725
|
+
validItems.push(ikey)
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
prompt += validItems.join(', ')
|
|
729
|
+
prompt += ', ?)'
|
|
730
|
+
break
|
|
731
|
+
}
|
|
732
|
+
case 'string':
|
|
733
|
+
prompt += ' (string, ?)'
|
|
734
|
+
break
|
|
735
|
+
case 'number':
|
|
736
|
+
prompt += ' (number, ?)'
|
|
737
|
+
break
|
|
738
|
+
case 'boolean':
|
|
739
|
+
prompt += ' (true, false, ?)'
|
|
740
|
+
break
|
|
741
|
+
// No default, the definition was already validated.
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (definition.default !== undefined) {
|
|
745
|
+
prompt += ` [${String(definition.default)}]`
|
|
746
|
+
}
|
|
747
|
+
prompt += ': '
|
|
748
|
+
|
|
749
|
+
const MAX_RETRIES = 42
|
|
750
|
+
let retryCount = 0
|
|
751
|
+
|
|
752
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
753
|
+
while (true) {
|
|
754
|
+
/* c8 ignore start - Defensive check. */
|
|
755
|
+
if (++retryCount > MAX_RETRIES) {
|
|
756
|
+
throw new InputError(
|
|
757
|
+
`Too many invalid attempts for property '${name}' ` +
|
|
758
|
+
`(limit: ${String(MAX_RETRIES)})`
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
/* c8 ignore stop */
|
|
762
|
+
|
|
763
|
+
const answer = (await rl.question(prompt)).trim()
|
|
764
|
+
// console.log('{' + answer + '}')
|
|
765
|
+
try {
|
|
766
|
+
const value = this._validatePropertyValue(name, answer)
|
|
767
|
+
// console.log('[' + value + ']')
|
|
768
|
+
config.properties[name] = value
|
|
769
|
+
break
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (error instanceof Error) {
|
|
772
|
+
this._log.trace(error.message)
|
|
773
|
+
}
|
|
774
|
+
this._process.stdout.write(`${definition.description}\n`)
|
|
775
|
+
if (definition.type === 'select') {
|
|
776
|
+
assert(definition.items, 'definition.items is required')
|
|
777
|
+
for (const [ikey, ival] of Object.entries(definition.items)) {
|
|
778
|
+
if (isString(ival)) {
|
|
779
|
+
this._process.stdout.write(`- ${ikey}: ${ival as string}\n`)
|
|
780
|
+
} else if (
|
|
781
|
+
isObject(ival) &&
|
|
782
|
+
this.isPlatformSupported(
|
|
783
|
+
(ival as InitTemplateItemValue).platforms
|
|
784
|
+
)
|
|
785
|
+
) {
|
|
786
|
+
this._process.stdout.write(
|
|
787
|
+
`- ${ikey}: ${(ival as InitTemplateItemValue).message}\n`
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Recursively copies all contents of a source folder to a destination folder.
|
|
799
|
+
*
|
|
800
|
+
* @remarks
|
|
801
|
+
* This internal method traverses the source folder structure and replicates
|
|
802
|
+
* it at the destination, copying all files and recursively processing
|
|
803
|
+
* subfolders.
|
|
804
|
+
*
|
|
805
|
+
* @param sourceFolderPath - The absolute path to the source folder.
|
|
806
|
+
* @param destinationFolderPath - The absolute path to the destination folder.
|
|
807
|
+
* @returns A promise that resolves when all contents have been copied.
|
|
808
|
+
*/
|
|
809
|
+
protected async _copyFolderRecursively({
|
|
810
|
+
sourceFolderPath,
|
|
811
|
+
destinationFolderPath,
|
|
812
|
+
}: {
|
|
813
|
+
sourceFolderPath: string
|
|
814
|
+
destinationFolderPath: string
|
|
815
|
+
}): Promise<void> {
|
|
816
|
+
// const log = this.log
|
|
817
|
+
|
|
818
|
+
await fs.mkdir(destinationFolderPath, { recursive: true })
|
|
819
|
+
|
|
820
|
+
const dirents = await fs.readdir(sourceFolderPath, {
|
|
821
|
+
withFileTypes: true,
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
for (const dirent of dirents) {
|
|
825
|
+
// log.trace(dirent.name)
|
|
826
|
+
|
|
827
|
+
if (dirent.isDirectory()) {
|
|
828
|
+
await this._copyFolderRecursively({
|
|
829
|
+
sourceFolderPath: path.join(sourceFolderPath, dirent.name),
|
|
830
|
+
destinationFolderPath: path.join(destinationFolderPath, dirent.name),
|
|
831
|
+
})
|
|
832
|
+
} else {
|
|
833
|
+
await fs.copyFile(
|
|
834
|
+
path.join(sourceFolderPath, dirent.name),
|
|
835
|
+
path.join(destinationFolderPath, dirent.name)
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Validates the structure and content of property definitions.
|
|
843
|
+
*
|
|
844
|
+
* @remarks
|
|
845
|
+
* This internal method performs comprehensive validation of the property
|
|
846
|
+
* definitions object during template construction, ensuring all definitions
|
|
847
|
+
* are well-formed and internally consistent before the template is used.
|
|
848
|
+
*
|
|
849
|
+
* Validation steps:
|
|
850
|
+
*
|
|
851
|
+
* <ol>
|
|
852
|
+
* <li><b>Overall structure:</b>
|
|
853
|
+
* <ul>
|
|
854
|
+
* <li>Verifies that <code>propertiesDefinitions</code> is an object.</li>
|
|
855
|
+
* <li>Ensures at least one property is defined (not empty).</li>
|
|
856
|
+
* </ul>
|
|
857
|
+
* </li>
|
|
858
|
+
* <li><b>Common property fields:</b>
|
|
859
|
+
* <ul>
|
|
860
|
+
* <li><code>label</code>: Must be a non-empty string.</li>
|
|
861
|
+
* <li><code>description</code>: Must be a non-empty string.</li>
|
|
862
|
+
* <li><code>isMandatory</code>: Must be a boolean if present.</li>
|
|
863
|
+
* <li><code>type</code>: Must be defined and one of: <code>select</code>,
|
|
864
|
+
* <code>string</code>, <code>number</code>, <code>boolean</code>.</li>
|
|
865
|
+
* </ul>
|
|
866
|
+
* </li>
|
|
867
|
+
* <li><b>Type-specific validation:</b>
|
|
868
|
+
* <ul>
|
|
869
|
+
* <li><b>Select properties:</b>
|
|
870
|
+
* <ul>
|
|
871
|
+
* <li>Must have an <code>items</code> object with at least one
|
|
872
|
+
* entry.</li>
|
|
873
|
+
* <li>Each item must be either a string (description) or an object with
|
|
874
|
+
* <code>platforms</code> array and <code>message</code> string.</li>
|
|
875
|
+
* <li>Non-mandatory properties must have a default value.</li>
|
|
876
|
+
* <li>Default values must be non-empty strings present in the items
|
|
877
|
+
* list.</li>
|
|
878
|
+
* </ul>
|
|
879
|
+
* </li>
|
|
880
|
+
* <li><b>String properties:</b> Default value must be a non-empty string
|
|
881
|
+
* if present.</li>
|
|
882
|
+
* <li><b>Number properties:</b> Default value must be a number if
|
|
883
|
+
* present.</li>
|
|
884
|
+
* <li><b>Boolean properties:</b> Default value must be a boolean if
|
|
885
|
+
* present.</li>
|
|
886
|
+
* </ul>
|
|
887
|
+
* </li>
|
|
888
|
+
* </ol>
|
|
889
|
+
*
|
|
890
|
+
* This validation ensures that templates are correctly configured before
|
|
891
|
+
* use, preventing runtime errors during property processing and interactive
|
|
892
|
+
* prompting. Any validation failure triggers an assertion error with a
|
|
893
|
+
* descriptive message indicating the specific problem.
|
|
894
|
+
*/
|
|
895
|
+
protected _validatePropertiesDefinitions(): void {
|
|
896
|
+
assert(
|
|
897
|
+
isObject(this._propertiesDefinitions),
|
|
898
|
+
'propertiesDefinitions is not an object.'
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
assert(
|
|
902
|
+
Object.keys(this._propertiesDefinitions).length > 0,
|
|
903
|
+
'propertiesDefinitions is an empty object.'
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
for (const [key, val] of Object.entries(this._propertiesDefinitions)) {
|
|
907
|
+
assert(isString(val.label), `Property '${key}' must have a string label`)
|
|
908
|
+
assert(val.label.trim() !== '', `Property '${key}' has an empty label`)
|
|
909
|
+
|
|
910
|
+
assert(
|
|
911
|
+
isString(val.description),
|
|
912
|
+
`Property '${key}' must have a string description`
|
|
913
|
+
)
|
|
914
|
+
assert(
|
|
915
|
+
val.description.trim() !== '',
|
|
916
|
+
`Property '${key}' has an empty description`
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
if (val.isMandatory !== undefined) {
|
|
920
|
+
assert(
|
|
921
|
+
isBoolean(val.isMandatory),
|
|
922
|
+
`Property '${key}' has a non boolean isMandatory value.`
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
927
|
+
assert(val.type !== undefined, `Property '${key}' has no type defined.`)
|
|
928
|
+
|
|
929
|
+
switch (val.type) {
|
|
930
|
+
case 'select':
|
|
931
|
+
assert(
|
|
932
|
+
val.items !== undefined,
|
|
933
|
+
`Property '${key}' of type 'select' has no items.`
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
assert(
|
|
937
|
+
isObject(val.items),
|
|
938
|
+
`Property '${key}' of type 'select' has invalid items.`
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
assert(
|
|
942
|
+
Object.keys(val.items).length !== 0,
|
|
943
|
+
`Property '${key}' of type 'select' has no items.`
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
for (const [ikey, ival] of Object.entries(val.items)) {
|
|
947
|
+
assert(
|
|
948
|
+
isString(ival) ||
|
|
949
|
+
(isObject(ival) &&
|
|
950
|
+
Array.isArray((ival as InitTemplateItemValue).platforms) &&
|
|
951
|
+
isString((ival as InitTemplateItemValue).message)),
|
|
952
|
+
`Property '${key}' has invalid item '${ikey}'.`
|
|
953
|
+
)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (!val.isMandatory) {
|
|
957
|
+
assert(
|
|
958
|
+
val.default !== undefined,
|
|
959
|
+
`Property '${key}' of type 'select' ` +
|
|
960
|
+
`must have a default value if not mandatory.`
|
|
961
|
+
)
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (val.default !== undefined) {
|
|
965
|
+
assert(
|
|
966
|
+
isString(val.default),
|
|
967
|
+
`Property '${key}' has a non string default value.`
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
assert(
|
|
971
|
+
(val.default as string).trim() !== '',
|
|
972
|
+
`Property '${key}' has an empty default value.`
|
|
973
|
+
)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (val.default !== undefined) {
|
|
977
|
+
assert(
|
|
978
|
+
Object.keys(val.items).includes(String(val.default)),
|
|
979
|
+
`Property '${key}' has a default value not in items list.`
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
break
|
|
983
|
+
|
|
984
|
+
case 'string':
|
|
985
|
+
if (val.default !== undefined) {
|
|
986
|
+
assert(
|
|
987
|
+
isString(val.default),
|
|
988
|
+
`Property '${key}' has a non string default value.`
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
assert(
|
|
992
|
+
(val.default as string).trim() !== '',
|
|
993
|
+
`Property '${key}' has an empty default value.`
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
break
|
|
997
|
+
|
|
998
|
+
case 'number':
|
|
999
|
+
if (val.default !== undefined) {
|
|
1000
|
+
assert(
|
|
1001
|
+
isNumber(val.default),
|
|
1002
|
+
`Property '${key}' has a non number default value.`
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
break
|
|
1006
|
+
|
|
1007
|
+
case 'boolean':
|
|
1008
|
+
if (val.default !== undefined) {
|
|
1009
|
+
assert(
|
|
1010
|
+
isBoolean(val.default),
|
|
1011
|
+
`Property '${key}' has a non boolean default value.`
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
break
|
|
1015
|
+
|
|
1016
|
+
default:
|
|
1017
|
+
assert(
|
|
1018
|
+
false,
|
|
1019
|
+
`Property '${key}' has unsupported type '${String(val.type)}'.`
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
break
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ----------------------------------------------------------------------------
|