generator-easy-ui5 3.5.0 → 3.5.1

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.
@@ -21,641 +21,608 @@ const AdmZip = require("adm-zip");
21
21
  const NPM_CONFIG_PREFIX = "easy-ui5_";
22
22
  let npmConfig;
23
23
  const getNPMConfig = (configName) => {
24
- if (!npmConfig) {
25
- npmConfig = require("libnpmconfig").read();
26
- }
27
- return npmConfig && npmConfig[`${NPM_CONFIG_PREFIX}${configName}`]
28
- }
24
+ if (!npmConfig) {
25
+ npmConfig = require("libnpmconfig").read();
26
+ }
27
+ return npmConfig && npmConfig[`${NPM_CONFIG_PREFIX}${configName}`];
28
+ };
29
29
 
30
30
  // the command line options of the generator
31
31
  const generatorOptions = {
32
- ghAuthToken: {
33
- type: String,
34
- description:
35
- "GitHub authToken to optionally access private generator repositories",
36
- npmConfig: true
37
- },
38
- ghOrg: {
39
- type: String,
40
- description: "GitHub organization to lookup for available generators",
41
- default: "ui5-community",
42
- hide: true // we don't want to recommend to use this option
43
- },
44
- subGeneratorPrefix: {
45
- type: String,
46
- description: "Prefix used for the lookup of the available generators",
47
- default: "generator-ui5-",
48
- hide: true // we don't want to recommend to use this option
49
- },
50
- addGhOrg: {
51
- type: String,
52
- description: `GitHub organization to lookup for additional available generators`,
53
- hide: true, // we don't want to recommend to use this option
54
- npmConfig: true
55
- },
56
- addSubGeneratorPrefix: {
57
- type: String,
58
- description: `Prefix used for the lookup of the additional available generators`,
59
- default: "generator-",
60
- hide: true, // we don't want to recommend to use this option
61
- npmConfig: true
62
- },
63
- list: {
64
- type: Boolean,
65
- description: "List the available subcommands of the generator",
66
- },
67
- skipUpdate: {
68
- type: Boolean,
69
- description: "Skip the update of the plugin generator",
70
- },
71
- forceUpdate: {
72
- type: Boolean,
73
- description: "Force the update of the plugin generator",
74
- },
75
- offline: {
76
- type: Boolean,
77
- alias: "o",
78
- description: "Running easy-ui5 in offline mode",
79
- },
80
- verbose: {
81
- type: Boolean,
82
- description: "Enable detailed logging",
83
- },
84
- plugins: {
85
- type: Boolean,
86
- alias: "p",
87
- description: "Get detailed version information",
88
- },
89
- next: {
90
- type: Boolean,
91
- description: "Preview the next mode to consume templates from bestofui5.org",
92
- },
32
+ ghAuthToken: {
33
+ type: String,
34
+ description: "GitHub authToken to optionally access private generator repositories",
35
+ npmConfig: true,
36
+ },
37
+ ghOrg: {
38
+ type: String,
39
+ description: "GitHub organization to lookup for available generators",
40
+ default: "ui5-community",
41
+ hide: true, // we don't want to recommend to use this option
42
+ },
43
+ subGeneratorPrefix: {
44
+ type: String,
45
+ description: "Prefix used for the lookup of the available generators",
46
+ default: "generator-ui5-",
47
+ hide: true, // we don't want to recommend to use this option
48
+ },
49
+ addGhOrg: {
50
+ type: String,
51
+ description: "GitHub organization to lookup for additional available generators",
52
+ hide: true, // we don't want to recommend to use this option
53
+ npmConfig: true,
54
+ },
55
+ addSubGeneratorPrefix: {
56
+ type: String,
57
+ description: "Prefix used for the lookup of the additional available generators",
58
+ default: "generator-",
59
+ hide: true, // we don't want to recommend to use this option
60
+ npmConfig: true,
61
+ },
62
+ list: {
63
+ type: Boolean,
64
+ description: "List the available subcommands of the generator",
65
+ },
66
+ skipUpdate: {
67
+ type: Boolean,
68
+ description: "Skip the update of the plugin generator",
69
+ },
70
+ forceUpdate: {
71
+ type: Boolean,
72
+ description: "Force the update of the plugin generator",
73
+ },
74
+ offline: {
75
+ type: Boolean,
76
+ alias: "o",
77
+ description: "Running easy-ui5 in offline mode",
78
+ },
79
+ verbose: {
80
+ type: Boolean,
81
+ description: "Enable detailed logging",
82
+ },
83
+ plugins: {
84
+ type: Boolean,
85
+ alias: "p",
86
+ description: "Get detailed version information",
87
+ },
88
+ next: {
89
+ type: Boolean,
90
+ description: "Preview the next mode to consume templates from bestofui5.org",
91
+ },
93
92
  };
94
93
 
95
94
  const generatorArgs = {
96
- generator: {
97
- type: String,
98
- required: false,
99
- description: "Name of the generator to invoke (without the \"generator-ui5-\" prefix)",
100
- },
101
- subcommand: {
102
- type: String,
103
- required: false,
104
- description: "Name of the subcommand to invoke (without the \"generator:\" prefix)",
105
- },
95
+ generator: {
96
+ type: String,
97
+ required: false,
98
+ description: 'Name of the generator to invoke (without the "generator-ui5-" prefix)',
99
+ },
100
+ subcommand: {
101
+ type: String,
102
+ required: false,
103
+ description: 'Name of the subcommand to invoke (without the "generator:" prefix)',
104
+ },
106
105
  };
107
106
 
108
107
  module.exports = class extends Generator {
109
- constructor(args, opts) {
110
- super(args, opts, {
111
- // disable the Yeoman 5 package-manager logic (auto install)!
112
- customInstallTask: "disabled"
113
- });
114
-
115
- Object.keys(generatorArgs).forEach((argName) => {
116
- // register the argument for being displayed in the help
117
- this.argument(argName, generatorArgs[argName]);
118
- });
119
-
120
- Object.keys(generatorOptions).forEach((optionName) => {
121
- const initialValue = this.options[optionName];
122
- // register the option for being displayed in the help
123
- this.option(optionName, generatorOptions[optionName]);
124
- const defaultedValue = this.options[optionName];
125
- if (generatorOptions[optionName].npmConfig) {
126
- // if a value is set, use the set value (parameter has higher precedence than npm config)
127
- // => this.option(...) applies the default value to this.options[...] used as last resort
128
- this.options[optionName] = initialValue || getNPMConfig(optionName) || defaultedValue;
129
- }
130
- });
131
- }
132
-
133
- _showBusy(statusText) {
134
- this._clearBusy();
135
- const progressChars = ["\\", "|", "/", "-"];
136
- let i = 0;
137
- process.stdout.write(`\r${statusText} `);
138
- this._busy = {
139
- text: statusText,
140
- timer: setInterval(() => {
141
- process.stdout.write(`\r${statusText} ${progressChars[i++]}`);
142
- i %= progressChars.length;
143
- }, 250),
144
- };
145
- }
146
-
147
- _clearBusy(newLine) {
148
- if (this._busy) {
149
- clearInterval(this._busy.timer);
150
- process.stdout.write("\r".padEnd(this._busy.text.length + 3) + (newLine ? "\n" : ""));
151
- delete this._busy;
152
- }
153
- }
154
-
155
- async prompting() {
156
-
157
- // Have Yeoman greet the user.
158
- if (!this.options.embedded) {
159
- this.log(yosay(`Welcome to the ${chalk.red("easy-ui5")} generator!`));
160
- }
161
-
162
- const home = path.join(__dirname, "..", "..");
163
-
164
- // check the permissions to Easy UI5s plugin directory which must
165
- // allow read/write to install additional plugin generators
166
- let pluginsHome = path.join(home, "plugin-generators");
167
- try {
168
- fs.accessSync(pluginsHome, fs.constants.R_OK | fs.constants.W_OK);
169
- } catch (e) {
170
- pluginsHome = path.join(require("os").homedir(), ".npm", "_generator-easy-ui5", "plugin-generators");
171
- if (this.options.verbose) {
172
- console.error(`Plugin directory: ${chalk.green(pluginsHome)}`);
173
- console.error(chalk.red(e.message));
174
- }
175
- fs.mkdirSync(pluginsHome, { recursive: true });
176
- }
177
-
178
- // log the plugins and configuration
179
- if (this.options.plugins) {
180
- const yeoman = require("yeoman-environment/package.json");
181
-
182
- const components = {
183
- "Node.js": process.version,
184
- "yeoman-environment": yeoman.version,
185
- "generator-easy-ui5": require(path.join(home, "package.json")).version,
186
- "home": home,
187
- "pluginsHome": pluginsHome,
188
- };
189
-
190
- Object.keys(components).forEach((component) => {
191
- this.log(`${chalk.green(component)}: ${components[component]}`);
192
- });
193
-
194
- this.log(chalk.green("\nAvailable generators:"));
195
- glob.sync(`${pluginsHome}/*/package.json`).forEach((plugin) => {
196
- const name = plugin.match(/.*\/generator-(.+)\/package\.json/)[1];
197
- const lib = require(plugin);
198
- this.log(` - ${chalk.green(name)}: ${lib.version}`);
199
- });
200
-
201
- return;
202
- }
203
-
204
- // create the octokit client to retrieve the generators from GH org
205
- // when not running in offline mode!
206
- let octokit;
207
- if (this.options.offline) {
208
- this.log(
209
- `Running in ${chalk.yellow(
210
- "offline"
211
- )} mode!`
212
- );
213
- } else {
214
- octokit = new MyOctokit({
215
- userAgent: `${this.rootGeneratorName()}:${this.rootGeneratorVersion()}`,
216
- auth: this.options.ghAuthToken,
217
- throttle: {
218
- onRateLimit: (retryAfter, options) => {
219
- this.log(
220
- `${chalk.yellow("Hit the GitHub API limit!")} Request quota exhausted for this request.`
221
- );
222
- if (options.request.retryCount === 0) {
223
- // only retries once
224
- this.log(`Retrying after ${retryAfter} seconds. Alternatively, you can cancel this operation and supply an auth token with the \`--ghAuthToken\` option. For more details, run \`yo easy-ui5 --help\`. `);
225
- return true;
226
- }
227
- },
228
- onAbuseLimit: () => {
229
- // does not retry, only logs a warning
230
- this.log(
231
- `${chalk.red("Hit the GitHub API limit again!")} Please supply an auth token with the \`--ghAuthToken\` option. For more details, run \`yo easy-ui5 --help\` `
232
- );
233
- },
234
- }
235
- });
236
- }
237
-
238
- // helper for filtering repos with corresponding subGenerator prefix
239
- const filterReposWithSubGeneratorPrefix = (repos, subGeneratorPrefix) => {
240
- if (!Array.isArray(repos)) {
241
- return [];
242
- }
243
- return repos.filter((repo) =>
244
- repo.name.startsWith(`${subGeneratorPrefix}`)
245
- ).map((repo) => {
246
- return {
247
- org: repo.owner?.login,
248
- name: repo.name,
249
- branch: repo.default_branch,
250
- subGeneratorName: repo.name.substring(subGeneratorPrefix.length),
251
- };
252
- });
253
- };
254
-
255
- // helper to retrieve the available repositories for a GH org
256
- const listGeneratorsForOrg = async (ghOrg, subGeneratorPrefix) => {
257
- const response = await octokit.repos.listForOrg({
258
- org: ghOrg,
259
- });
260
- return filterReposWithSubGeneratorPrefix(response?.data, subGeneratorPrefix);
261
- };
262
-
263
- // helper to retrieve the available repositories for a GH user
264
- const listGeneratorsForUser = async (ghUser, subGeneratorPrefix) => {
265
- const response = await octokit.repos.listForUser({
266
- username: ghUser,
267
- });
268
- return filterReposWithSubGeneratorPrefix(response?.data, subGeneratorPrefix);
269
- };
270
-
271
- // determine the generator to be used
272
- let generator;
273
-
274
- // try to identify whether concrete generator is defined
275
- if (!generator) {
276
- // determine generator by ${owner}/${repo}(!${dir})? syntax, e.g.:
277
- // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial
278
- // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial#1.0
279
- // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator
280
- // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator#1.0
281
- const reGenerator = /([^\/]+)\/([^\!\#]+)(?:\!([^\#]+))?(?:\#(.+))?/;
282
- const matchGenerator = reGenerator.exec(this.options.generator);
283
- if (matchGenerator) {
284
- // derive and path the generator information from command line
285
- const [owner, repo, dir = "/generator", branch] = matchGenerator.slice(1);
286
- generator = {
287
- org: owner,
288
- name: repo,
289
- branch,
290
- dir,
291
- pluginPath: `_/${owner}/${repo}`,
292
- };
293
- // log which generator is being used!
294
- if (this.options.verbose) {
295
- this.log(
296
- `Using generator ${chalk.green(
297
- `${owner}/${repo}!${dir}${branch ? "#" + branch : ""}`
298
- )}`
299
- );
300
- }
301
- }
302
- }
303
-
304
- // retrieve the available repositories (if no generator is specified specified directly)
305
- let availGenerators;
306
- if (!generator) {
307
- if (this.options.offline) {
308
- availGenerators = glob.sync(`${pluginsHome}/generator-*/package.json`).map((plugin) => {
309
- const match = plugin.match(/.*\/(generator-(.+))\/package\.json/);
310
- return {
311
- org: "local",
312
- name: match[1],
313
- subGeneratorName: match[2].match(/(?:ui5-)?(.*)/)?.[1] || match[2],
314
- local: true,
315
- }
316
- });
317
- } else {
318
- if (this.options.next) {
319
- // check bestofui5.org for generators
320
- try {
321
- const response = await request({
322
- method: "GET",
323
- url: "https://raw.githubusercontent.com/ui5-community/bestofui5-data/live-data/data/data.json",
324
- });
325
- const data = JSON.parse(response.data);
326
-
327
- availGenerators = data?.packages?.filter(entry => {
328
- return entry.type === "generator";
329
- }).map(entry => {
330
- return {
331
- org: entry.gitHubOwner,
332
- name: entry.gitHubRepo,
333
- subGeneratorName: entry.gitHubRepo.match(/(?:generator-(?:ui5-)?)(.*)/)?.[1] || entry.gitHubRepo,
334
- };
335
- });
336
- } catch (e) {
337
- console.error(`Failed to connect to bestofui5.org to retrieve all available generators! Run with --verbose for details!`);
338
- if (this.options.verbose) {
339
- console.error(e);
340
- }
341
- return;
342
- }
343
- } else {
344
- // check the main GH org for generators
345
- try {
346
- availGenerators = await listGeneratorsForOrg(this.options.ghOrg, this.options.subGeneratorPrefix);
347
- } catch (e) {
348
- console.error(`Failed to connect to GitHub to retrieve all available generators for "${this.options.ghOrg}" organization! Run with --verbose for details!`);
349
- if (this.options.verbose) {
350
- console.error(e);
351
- }
352
- return;
353
- }
354
-
355
- // check the additional GH org for generators with a different prefix
356
- try {
357
- if (this.options.addGhOrg && this.options.addSubGeneratorPrefix) {
358
- availGenerators = availGenerators.concat(await listGeneratorsForOrg(this.options.addGhOrg, this.options.addSubGeneratorPrefix));
359
- }
360
- } catch (e) {
361
- if (this.options.verbose) {
362
- this.log(`Failed to connect to GitHub retrieve additional generators for "${this.options.addGhOrg}" organization! Try to retrieve for user...`);
363
- }
364
- try {
365
- availGenerators = availGenerators.concat(await listGeneratorsForUser(this.options.addGhOrg, this.options.addSubGeneratorPrefix));
366
- } catch (e) {
367
- console.error(`Failed to connect to GitHub to retrieve additional generators for organization or user "${this.options.addGhOrg}"! Run with --verbose for details!`);
368
- if (this.options.verbose) {
369
- console.error(e);
370
- }
371
- return;
372
- }
373
- }
374
- }
375
- }
376
- }
377
-
378
- // if no generator is provided and doesn't exist, ask for generator name
379
- if (!generator) {
380
- // check for provided generator being available on GH
381
- generator = this.options.generator && availGenerators.find((repo) =>
382
- repo.subGeneratorName === this.options.generator
383
- );
384
-
385
- // if no generator is provided and doesn't exist, ask for generator name
386
- if (this.options.generator && !generator) {
387
- this.log(
388
- `The generator ${chalk.red(
389
- this.options.generator
390
- )} was not found. Please select an existing generator!`
391
- );
392
- }
393
-
394
- // still not found, select a generator
395
- if (!generator) {
396
- const generatorIdx = (
397
- await this.prompt([
398
- {
399
- type: "list",
400
- name: "generator",
401
- message: "Select your generator?",
402
- choices: availGenerators.map((availGenerator, idx) => ({
403
- name: `${availGenerator.subGeneratorName}${this.options.addGhOrg ? ` [${availGenerator.org}]` : ""}`,
404
- value: idx,
405
- })),
406
- },
407
- ])
408
- ).generator;
409
- generator = availGenerators[generatorIdx];
410
- }
411
- }
412
-
413
- let generatorPath = path.join(pluginsHome, generator.pluginPath || generator.name);
414
- if (!this.options.offline) {
415
- // lookup the default path of the generator if not set
416
- if (!generator.branch) {
417
- try {
418
- const repoInfo = await octokit.repos.get({
419
- owner: generator.org,
420
- repo: generator.name,
421
- });
422
- generator.branch = repoInfo.data.default_branch;
423
- } catch (e) {
424
- console.error(`Generator "${owner}/${repo}!${dir}${branch ? "#" + branch : ""}" not found! Run with --verbose for details!`);
425
- if (this.options.verbose) {
426
- console.error(e);
427
- }
428
- return;
429
- }
430
- }
431
- // fetch the branch to retrieve the latest commit SHA
432
- let commitSHA;
433
- try {
434
- // determine the commitSHA
435
- const reqBranch = await octokit.repos.getBranch({
436
- owner: generator.org,
437
- repo: generator.name,
438
- branch: generator.branch,
439
- });
440
- commitSHA = reqBranch.data.commit.sha;
441
- } catch (e) {
442
- console.error(chalk.red(`Failed to retrieve the branch "${generator.branch}" for repository "${generator.name}" for "${generator.org}" organization! Run with --verbose for details!`));
443
- if (this.options.verbose) {
444
- console.error(chalk.red(e.message));
445
- }
446
- return;
447
- }
448
-
449
- if (this.options.verbose) {
450
- this.log(
451
- `Using commit ${commitSHA} from @${generator.org}/${generator.name}#${generator.branch}...`
452
- );
453
- }
454
- const shaMarker = path.join(generatorPath, `.${commitSHA}`);
455
-
456
- if (fs.existsSync(generatorPath) && !this.options.skipUpdate) {
457
- // check if the SHA marker exists to know whether the generator is up-to-date or not
458
- if (this.options.forceUpdate || !fs.existsSync(shaMarker)) {
459
- if (this.options.verbose) {
460
- this.log(`Generator "${generator.name}" in "${generatorPath}" is outdated...`);
461
- }
462
- // remove if the SHA marker doesn't exist => outdated!
463
- this._showBusy(` Removing old "${generator.name}" templates`);
464
- await rm(generatorPath, { recursive: true });
465
- }
466
- }
467
-
468
- // re-fetch the generator and extract into local plugin folder
469
- if (!fs.existsSync(generatorPath)) {
470
- if (this.options.verbose) {
471
- this.log(`Extracting ZIP to "${generatorPath}"...`);
472
- }
473
- this._showBusy(` Downloading and extracting "${generator.name}" templates`);
474
- const reqZIPArchive = await octokit.repos.downloadZipballArchive({
475
- owner: generator.org,
476
- repo: generator.name,
477
- ref: commitSHA,
478
- });
479
- const buffer = Buffer.from(new Uint8Array(reqZIPArchive.data));
480
- const zip = new AdmZip(buffer);
481
- const zipEntries = zip.getEntries();
482
- zipEntries.forEach((entry) => {
483
- const match =
484
- !entry.isDirectory && entry.entryName.match(/[^\/]+(\/.+)/);
485
- let entryPath;
486
- if (generator.dir && match && match[1].startsWith(generator.dir)) {
487
- entryPath = path.dirname(match[1].substring(generator.dir.length));
488
- } else if (!generator.dir && match) {
489
- entryPath = path.dirname(match[1]);
490
- }
491
- if (entryPath) {
492
- zip.extractEntryTo(
493
- entry,
494
- path.join(generatorPath, entryPath),
495
- false,
496
- true
497
- );
498
- }
499
- });
500
- fs.writeFileSync(shaMarker, commitSHA);
501
-
502
- // run yarn/npm install
503
- if (this.options.verbose) {
504
- this.log("Installing the plugin dependencies...");
505
- }
506
- this._showBusy(` Preparing "${generator.name}"`);
507
- await new Promise(function (resolve, reject) {
508
- spawn((hasYarn() ? "yarn" : "npm"), ["install", "--no-progress", "--ignore-engines"], {
509
- stdio: this.config.verbose ? "inherit" : "ignore",
510
- cwd: generatorPath,
511
- env: {
512
- ...process.env,
513
- "NO_UPDATE_NOTIFIER": true
514
- }
515
- }).on("exit", function (code) {
516
- resolve(code);
517
- }).on("error", function (err) {
518
- reject(err);
519
- });
520
- }.bind(this));
521
- }
522
-
523
- this._clearBusy(true);
524
-
525
- }
526
-
527
- // filter the local options and the help command
528
- const opts = Object.keys(this._options).filter(
529
- (optionName) =>
530
- !(generatorOptions.hasOwnProperty(optionName) || optionName === "help")
531
- );
532
-
533
- // create the env for the plugin generator
534
- const yeoman = require("yeoman-environment");
535
- const env = yeoman.createEnv(this.args, opts);
536
-
537
- // helper to derive the subcommand
538
- function deriveSubcommand(namespace) {
539
- const match = namespace.match(/[^:]+:(.+)/);
540
- return match ? match[1] : namespace;
541
- }
542
-
543
- // filter the hidden subgenerators already
544
- // -> subgenerators must be found in env as they are returned by lookup!
545
- let subGenerators = env
546
- .lookup({ localOnly: true, packagePaths: generatorPath })
547
- .filter((sub) => {
548
- const subGenerator = env.get(sub.namespace);
549
- return !subGenerator.hidden;
550
- });
551
-
552
- // list the available subgenerators in the console (as help)
553
- if (this.options.list) {
554
- let maxLength = 0;
555
- this.log(subGenerators
556
- .map(sub => {
557
- maxLength = Math.max(sub.namespace.length, maxLength);
558
- return sub;
559
- })
560
- .reduce((output, sub) => {
561
- const subGenerator = env.get(sub.namespace);
562
- const displayName = subGenerator.displayName || "";
563
- let line = ` ${deriveSubcommand(sub.namespace).padEnd(maxLength + 2)}`;
564
- if (displayName) {
565
- line += ` # ${subGenerator.displayName}`;
566
- }
567
- return `${output}\n${line}`;
568
- }, `Subcommands (${subGenerators.length}):`));
569
- return;
570
- }
571
-
572
- // if a subcommand is provided as argument, identify the matching subgenerator
573
- // and remove the rest of the subgenerators from the list for later steps
574
- if (this.options.subcommand) {
575
- const selectedSubGenerator = subGenerators
576
- .filter((sub) => {
577
- // identify the subgenerator by subcommand
578
- return new RegExp(`:${this.options.subcommand}$`).test(sub.namespace);
579
- });
580
- if (selectedSubGenerator.length == 1) {
581
- subGenerators = selectedSubGenerator;
582
- } else {
583
- this.log(
584
- `The generator ${chalk.red(
585
- this.options.generator
586
- )} has no subcommand ${chalk.red(
587
- this.options.subcommand
588
- )}. Please select an existing subcommand!`
589
- );
590
- }
591
- }
592
-
593
- // transform the list of the subgenerators and identify the
594
- // default subgenerator for the default selection
595
- let defaultSubGenerator;
596
- let maxLength = 0;
597
- subGenerators = subGenerators
598
- .map(sub => {
599
- const generator = env.get(sub.namespace);
600
- let subcommand = deriveSubcommand(sub.namespace);
601
- let displayName = generator.displayName || subcommand;
602
- maxLength = Math.max(displayName.length, maxLength);
603
- return {
604
- subcommand,
605
- displayName,
606
- sub,
607
- };
608
- })
609
- .map(({ subcommand, displayName, sub }) => {
610
- const transformed = {
611
- name: `${displayName.padEnd(maxLength + 2)} [${subcommand}]`,
612
- value: sub.namespace,
613
- };
614
- if (/:app$/.test(sub.namespace)) {
615
- defaultSubGenerator = transformed;
616
- }
617
- return transformed;
618
- });
619
-
620
- // at least 1 subgenerator must be present
621
- if (subGenerators.length >= 1) {
622
-
623
- // by default the 1st subgenerator is used
624
- let subGenerator = subGenerators[0].value;
625
-
626
- // if more than 1 subgenerator is present
627
- // ask the developer to select one!
628
- if (subGenerators.length > 1) {
629
- subGenerator = (
630
- await this.prompt([
631
- {
632
- type: "list",
633
- name: "subGenerator",
634
- message: "What do you want to do?",
635
- default: defaultSubGenerator && defaultSubGenerator.value,
636
- choices: subGenerators,
637
- },
638
- ])
639
- ).subGenerator;
640
- }
641
-
642
- if (this.options.verbose) {
643
- this.log(`Calling ${chalk.red(subGenerator)}...\n \\_ in: ${generatorPath}`);
644
- }
645
-
646
- // finally, run the subgenerator
647
- env.run(subGenerator, {
648
- verbose: this.options.verbose,
649
- embedded: true,
650
- });
651
-
652
- } else {
653
- this.log(
654
- `The generator ${chalk.red(
655
- this.options.generator
656
- )} has no visible subgenerators!`
657
- );
658
- }
659
-
660
- }
108
+ constructor(args, opts) {
109
+ super(args, opts, {
110
+ // disable the Yeoman 5 package-manager logic (auto install)!
111
+ customInstallTask: "disabled",
112
+ });
113
+
114
+ Object.keys(generatorArgs).forEach((argName) => {
115
+ // register the argument for being displayed in the help
116
+ this.argument(argName, generatorArgs[argName]);
117
+ });
118
+
119
+ Object.keys(generatorOptions).forEach((optionName) => {
120
+ const initialValue = this.options[optionName];
121
+ // register the option for being displayed in the help
122
+ this.option(optionName, generatorOptions[optionName]);
123
+ const defaultedValue = this.options[optionName];
124
+ if (generatorOptions[optionName].npmConfig) {
125
+ // if a value is set, use the set value (parameter has higher precedence than npm config)
126
+ // => this.option(...) applies the default value to this.options[...] used as last resort
127
+ this.options[optionName] = initialValue || getNPMConfig(optionName) || defaultedValue;
128
+ }
129
+ });
130
+ }
131
+
132
+ _showBusy(statusText) {
133
+ this._clearBusy();
134
+ const progressChars = ["\\", "|", "/", "-"];
135
+ let i = 0;
136
+ process.stdout.write(`\r${statusText} `);
137
+ this._busy = {
138
+ text: statusText,
139
+ timer: setInterval(() => {
140
+ process.stdout.write(`\r${statusText} ${progressChars[i++]}`);
141
+ i %= progressChars.length;
142
+ }, 250),
143
+ };
144
+ }
145
+
146
+ _clearBusy(newLine) {
147
+ if (this._busy) {
148
+ clearInterval(this._busy.timer);
149
+ process.stdout.write("\r".padEnd(this._busy.text.length + 3) + (newLine ? "\n" : ""));
150
+ delete this._busy;
151
+ }
152
+ }
153
+
154
+ async prompting() {
155
+ const home = path.join(__dirname, "..", "..");
156
+ const pkgJson = require(path.join(home, "package.json"));
157
+
158
+ // Have Yeoman greet the user.
159
+ if (!this.options.embedded) {
160
+ this.log(yosay(`Welcome to the ${chalk.red("easy-ui5")} ${chalk.yellow(pkgJson.version)} generator!`));
161
+ }
162
+
163
+ // check the permissions to Easy UI5s plugin directory which must
164
+ // allow read/write to install additional plugin generators
165
+ let pluginsHome = path.join(home, "plugin-generators");
166
+ try {
167
+ fs.accessSync(pluginsHome, fs.constants.R_OK | fs.constants.W_OK);
168
+ } catch (e) {
169
+ pluginsHome = path.join(require("os").homedir(), ".npm", "_generator-easy-ui5", "plugin-generators");
170
+ if (this.options.verbose) {
171
+ console.error(`Plugin directory: ${chalk.green(pluginsHome)}`);
172
+ console.error(chalk.red(e.message));
173
+ }
174
+ fs.mkdirSync(pluginsHome, { recursive: true });
175
+ }
176
+
177
+ // log the plugins and configuration
178
+ if (this.options.plugins) {
179
+ const yeoman = require("yeoman-environment/package.json");
180
+
181
+ const components = {
182
+ "Node.js": process.version,
183
+ "yeoman-environment": yeoman.version,
184
+ "generator-easy-ui5": pkgJson.version,
185
+ home: home,
186
+ pluginsHome: pluginsHome,
187
+ };
188
+
189
+ Object.keys(components).forEach((component) => {
190
+ this.log(`${chalk.green(component)}: ${components[component]}`);
191
+ });
192
+
193
+ this.log(chalk.green("\nAvailable generators:"));
194
+ glob.sync(`${pluginsHome}/*/package.json`).forEach((plugin) => {
195
+ const name = plugin.match(/.*\/generator-(.+)\/package\.json/)[1];
196
+ const lib = require(plugin);
197
+ this.log(` - ${chalk.green(name)}: ${lib.version}`);
198
+ });
199
+
200
+ return;
201
+ }
202
+
203
+ // create the octokit client to retrieve the generators from GH org
204
+ // when not running in offline mode!
205
+ let octokit;
206
+ if (this.options.offline) {
207
+ this.log(`Running in ${chalk.yellow("offline")} mode!`);
208
+ } else {
209
+ octokit = new MyOctokit({
210
+ userAgent: `${this.rootGeneratorName()}:${this.rootGeneratorVersion()}`,
211
+ auth: this.options.ghAuthToken,
212
+ throttle: {
213
+ onRateLimit: (retryAfter, options) => {
214
+ this.log(`${chalk.yellow("Hit the GitHub API limit!")} Request quota exhausted for this request.`);
215
+ if (options.request.retryCount === 0) {
216
+ // only retries once
217
+ this.log(
218
+ `Retrying after ${retryAfter} seconds. Alternatively, you can cancel this operation and supply an auth token with the \`--ghAuthToken\` option. For more details, run \`yo easy-ui5 --help\`. `
219
+ );
220
+ return true;
221
+ }
222
+ },
223
+ onAbuseLimit: () => {
224
+ // does not retry, only logs a warning
225
+ this.log(`${chalk.red("Hit the GitHub API limit again!")} Please supply an auth token with the \`--ghAuthToken\` option. For more details, run \`yo easy-ui5 --help\` `);
226
+ },
227
+ },
228
+ });
229
+ }
230
+
231
+ // helper for filtering repos with corresponding subGenerator prefix
232
+ const filterReposWithSubGeneratorPrefix = (repos, subGeneratorPrefix) => {
233
+ if (!Array.isArray(repos)) {
234
+ return [];
235
+ }
236
+ return repos
237
+ .filter((repo) => repo.name.startsWith(`${subGeneratorPrefix}`))
238
+ .map((repo) => {
239
+ return {
240
+ org: repo.owner?.login,
241
+ name: repo.name,
242
+ branch: repo.default_branch,
243
+ subGeneratorName: repo.name.substring(subGeneratorPrefix.length),
244
+ };
245
+ });
246
+ };
247
+
248
+ // helper to retrieve the available repositories for a GH org
249
+ const listGeneratorsForOrg = async (ghOrg, subGeneratorPrefix) => {
250
+ const response = await octokit.repos.listForOrg({
251
+ org: ghOrg,
252
+ });
253
+ return filterReposWithSubGeneratorPrefix(response?.data, subGeneratorPrefix);
254
+ };
255
+
256
+ // helper to retrieve the available repositories for a GH user
257
+ const listGeneratorsForUser = async (ghUser, subGeneratorPrefix) => {
258
+ const response = await octokit.repos.listForUser({
259
+ username: ghUser,
260
+ });
261
+ return filterReposWithSubGeneratorPrefix(response?.data, subGeneratorPrefix);
262
+ };
263
+
264
+ // determine the generator to be used
265
+ let generator;
266
+
267
+ // try to identify whether concrete generator is defined
268
+ if (!generator) {
269
+ // determine generator by ${owner}/${repo}(!${dir})? syntax, e.g.:
270
+ // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial
271
+ // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial#1.0
272
+ // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator
273
+ // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator#1.0
274
+ const reGenerator = /([^\/]+)\/([^\!\#]+)(?:\!([^\#]+))?(?:\#(.+))?/;
275
+ const matchGenerator = reGenerator.exec(this.options.generator);
276
+ if (matchGenerator) {
277
+ // derive and path the generator information from command line
278
+ const [owner, repo, dir = "/generator", branch] = matchGenerator.slice(1);
279
+ generator = {
280
+ org: owner,
281
+ name: repo,
282
+ branch,
283
+ dir,
284
+ pluginPath: `_/${owner}/${repo}`,
285
+ };
286
+ // log which generator is being used!
287
+ if (this.options.verbose) {
288
+ this.log(`Using generator ${chalk.green(`${owner}/${repo}!${dir}${branch ? "#" + branch : ""}`)}`);
289
+ }
290
+ }
291
+ }
292
+
293
+ // retrieve the available repositories (if no generator is specified specified directly)
294
+ let availGenerators;
295
+ if (!generator) {
296
+ if (this.options.offline) {
297
+ availGenerators = glob.sync(`${pluginsHome}/generator-*/package.json`).map((plugin) => {
298
+ const match = plugin.match(/.*\/(generator-(.+))\/package\.json/);
299
+ return {
300
+ org: "local",
301
+ name: match[1],
302
+ subGeneratorName: match[2].match(/(?:ui5-)?(.*)/)?.[1] || match[2],
303
+ local: true,
304
+ };
305
+ });
306
+ } else {
307
+ if (this.options.next) {
308
+ // check bestofui5.org for generators
309
+ try {
310
+ const response = await request({
311
+ method: "GET",
312
+ url: "https://raw.githubusercontent.com/ui5-community/bestofui5-data/live-data/data/data.json",
313
+ });
314
+ const data = JSON.parse(response.data);
315
+
316
+ availGenerators = data?.packages
317
+ ?.filter((entry) => {
318
+ return entry.type === "generator";
319
+ })
320
+ .map((entry) => {
321
+ return {
322
+ org: entry.gitHubOwner,
323
+ name: entry.gitHubRepo,
324
+ subGeneratorName: entry.gitHubRepo.match(/(?:generator-(?:ui5-)?)(.*)/)?.[1] || entry.gitHubRepo,
325
+ };
326
+ });
327
+ } catch (e) {
328
+ console.error("Failed to connect to bestofui5.org to retrieve all available generators! Run with --verbose for details!");
329
+ if (this.options.verbose) {
330
+ console.error(e);
331
+ }
332
+ return;
333
+ }
334
+ } else {
335
+ // check the main GH org for generators
336
+ try {
337
+ availGenerators = await listGeneratorsForOrg(this.options.ghOrg, this.options.subGeneratorPrefix);
338
+ } catch (e) {
339
+ console.error(`Failed to connect to GitHub to retrieve all available generators for "${this.options.ghOrg}" organization! Run with --verbose for details!`);
340
+ if (this.options.verbose) {
341
+ console.error(e);
342
+ }
343
+ return;
344
+ }
345
+
346
+ // check the additional GH org for generators with a different prefix
347
+ try {
348
+ if (this.options.addGhOrg && this.options.addSubGeneratorPrefix) {
349
+ availGenerators = availGenerators.concat(await listGeneratorsForOrg(this.options.addGhOrg, this.options.addSubGeneratorPrefix));
350
+ }
351
+ } catch (e) {
352
+ if (this.options.verbose) {
353
+ this.log(`Failed to connect to GitHub retrieve additional generators for "${this.options.addGhOrg}" organization! Try to retrieve for user...`);
354
+ }
355
+ try {
356
+ availGenerators = availGenerators.concat(await listGeneratorsForUser(this.options.addGhOrg, this.options.addSubGeneratorPrefix));
357
+ } catch (e1) {
358
+ console.error(`Failed to connect to GitHub to retrieve additional generators for organization or user "${this.options.addGhOrg}"! Run with --verbose for details!`);
359
+ if (this.options.verbose) {
360
+ console.error(e1);
361
+ }
362
+ return;
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+
369
+ // if no generator is provided and doesn't exist, ask for generator name
370
+ if (!generator) {
371
+ // check for provided generator being available on GH
372
+ generator = this.options.generator && availGenerators.find((repo) => repo.subGeneratorName === this.options.generator);
373
+
374
+ // if no generator is provided and doesn't exist, ask for generator name
375
+ if (this.options.generator && !generator) {
376
+ this.log(`The generator ${chalk.red(this.options.generator)} was not found. Please select an existing generator!`);
377
+ }
378
+
379
+ // still not found, select a generator
380
+ if (!generator) {
381
+ const generatorIdx = (
382
+ await this.prompt([
383
+ {
384
+ type: "list",
385
+ name: "generator",
386
+ message: "Select your generator?",
387
+ choices: availGenerators.map((availGenerator, idx) => ({
388
+ name: `${availGenerator.subGeneratorName}${this.options.addGhOrg ? ` [${availGenerator.org}]` : ""}`,
389
+ value: idx,
390
+ })),
391
+ },
392
+ ])
393
+ ).generator;
394
+ generator = availGenerators[generatorIdx];
395
+ }
396
+ }
397
+
398
+ let generatorPath = path.join(pluginsHome, generator.pluginPath || generator.name);
399
+ if (!this.options.offline) {
400
+ // lookup the default path of the generator if not set
401
+ if (!generator.branch) {
402
+ try {
403
+ const repoInfo = await octokit.repos.get({
404
+ owner: generator.org,
405
+ repo: generator.name,
406
+ });
407
+ generator.branch = repoInfo.data.default_branch;
408
+ } catch (e) {
409
+ console.error(`Generator "${owner}/${repo}!${dir}${branch ? "#" + branch : ""}" not found! Run with --verbose for details!`);
410
+ if (this.options.verbose) {
411
+ console.error(e);
412
+ }
413
+ return;
414
+ }
415
+ }
416
+ // fetch the branch to retrieve the latest commit SHA
417
+ let commitSHA;
418
+ try {
419
+ // determine the commitSHA
420
+ const reqBranch = await octokit.repos.getBranch({
421
+ owner: generator.org,
422
+ repo: generator.name,
423
+ branch: generator.branch,
424
+ });
425
+ commitSHA = reqBranch.data.commit.sha;
426
+ } catch (e) {
427
+ console.error(chalk.red(`Failed to retrieve the branch "${generator.branch}" for repository "${generator.name}" for "${generator.org}" organization! Run with --verbose for details!`));
428
+ if (this.options.verbose) {
429
+ console.error(chalk.red(e.message));
430
+ }
431
+ return;
432
+ }
433
+
434
+ if (this.options.verbose) {
435
+ this.log(`Using commit ${commitSHA} from @${generator.org}/${generator.name}#${generator.branch}...`);
436
+ }
437
+ const shaMarker = path.join(generatorPath, `.${commitSHA}`);
438
+
439
+ if (fs.existsSync(generatorPath) && !this.options.skipUpdate) {
440
+ // check if the SHA marker exists to know whether the generator is up-to-date or not
441
+ if (this.options.forceUpdate || !fs.existsSync(shaMarker)) {
442
+ if (this.options.verbose) {
443
+ this.log(`Generator "${generator.name}" in "${generatorPath}" is outdated...`);
444
+ }
445
+ // remove if the SHA marker doesn't exist => outdated!
446
+ this._showBusy(` Removing old "${generator.name}" templates`);
447
+ await rm(generatorPath, { recursive: true });
448
+ }
449
+ }
450
+
451
+ // re-fetch the generator and extract into local plugin folder
452
+ if (!fs.existsSync(generatorPath)) {
453
+ if (this.options.verbose) {
454
+ this.log(`Extracting ZIP to "${generatorPath}"...`);
455
+ }
456
+ this._showBusy(` Downloading and extracting "${generator.name}" templates`);
457
+ const reqZIPArchive = await octokit.repos.downloadZipballArchive({
458
+ owner: generator.org,
459
+ repo: generator.name,
460
+ ref: commitSHA,
461
+ });
462
+ const buffer = Buffer.from(new Uint8Array(reqZIPArchive.data));
463
+ const zip = new AdmZip(buffer);
464
+ const zipEntries = zip.getEntries();
465
+ zipEntries.forEach((entry) => {
466
+ const match = !entry.isDirectory && entry.entryName.match(/[^\/]+(\/.+)/);
467
+ let entryPath;
468
+ if (generator.dir && match && match[1].startsWith(generator.dir)) {
469
+ entryPath = path.dirname(match[1].substring(generator.dir.length));
470
+ } else if (!generator.dir && match) {
471
+ entryPath = path.dirname(match[1]);
472
+ }
473
+ if (entryPath) {
474
+ zip.extractEntryTo(entry, path.join(generatorPath, entryPath), false, true);
475
+ }
476
+ });
477
+ fs.writeFileSync(shaMarker, commitSHA);
478
+
479
+ // run yarn/npm install
480
+ if (this.options.verbose) {
481
+ this.log("Installing the plugin dependencies...");
482
+ }
483
+ this._showBusy(` Preparing "${generator.name}"`);
484
+ await new Promise(
485
+ function (resolve, reject) {
486
+ spawn(hasYarn() ? "yarn" : "npm", ["install", "--no-progress", "--ignore-engines"], {
487
+ stdio: this.config.verbose ? "inherit" : "ignore",
488
+ cwd: generatorPath,
489
+ env: {
490
+ ...process.env,
491
+ NO_UPDATE_NOTIFIER: true,
492
+ },
493
+ })
494
+ .on("exit", function (code) {
495
+ resolve(code);
496
+ })
497
+ .on("error", function (err) {
498
+ reject(err);
499
+ });
500
+ }.bind(this)
501
+ );
502
+ }
503
+
504
+ this._clearBusy(true);
505
+ }
506
+
507
+ // filter the local options and the help command
508
+ const opts = Object.keys(this._options).filter((optionName) => !(generatorOptions.hasOwnProperty(optionName) || optionName === "help"));
509
+
510
+ // create the env for the plugin generator
511
+ let env = this.env; // in case of Yeoman UI the env is injected!
512
+ if (!env) {
513
+ const yeoman = require("yeoman-environment");
514
+ env = yeoman.createEnv(this.args, opts);
515
+ }
516
+
517
+ // helper to derive the subcommand
518
+ function deriveSubcommand(namespace) {
519
+ const match = namespace.match(/[^:]+:(.+)/);
520
+ return match ? match[1] : namespace;
521
+ }
522
+
523
+ // filter the hidden subgenerators already
524
+ // -> subgenerators must be found in env as they are returned by lookup!
525
+ let subGenerators = env.lookup({ localOnly: true, packagePaths: generatorPath }).filter((sub) => {
526
+ const subGenerator = env.get(sub.namespace);
527
+ return !subGenerator.hidden;
528
+ });
529
+
530
+ // list the available subgenerators in the console (as help)
531
+ if (this.options.list) {
532
+ let maxLength = 0;
533
+ this.log(
534
+ subGenerators
535
+ .map((sub) => {
536
+ maxLength = Math.max(sub.namespace.length, maxLength);
537
+ return sub;
538
+ })
539
+ .reduce((output, sub) => {
540
+ const subGenerator = env.get(sub.namespace);
541
+ const displayName = subGenerator.displayName || "";
542
+ let line = ` ${deriveSubcommand(sub.namespace).padEnd(maxLength + 2)}`;
543
+ if (displayName) {
544
+ line += ` # ${subGenerator.displayName}`;
545
+ }
546
+ return `${output}\n${line}`;
547
+ }, `Subcommands (${subGenerators.length}):`)
548
+ );
549
+ return;
550
+ }
551
+
552
+ // if a subcommand is provided as argument, identify the matching subgenerator
553
+ // and remove the rest of the subgenerators from the list for later steps
554
+ if (this.options.subcommand) {
555
+ const selectedSubGenerator = subGenerators.filter((sub) => {
556
+ // identify the subgenerator by subcommand
557
+ return new RegExp(`:${this.options.subcommand}$`).test(sub.namespace);
558
+ });
559
+ if (selectedSubGenerator.length == 1) {
560
+ subGenerators = selectedSubGenerator;
561
+ } else {
562
+ this.log(`The generator ${chalk.red(this.options.generator)} has no subcommand ${chalk.red(this.options.subcommand)}. Please select an existing subcommand!`);
563
+ }
564
+ }
565
+
566
+ // transform the list of the subgenerators and identify the
567
+ // default subgenerator for the default selection
568
+ let defaultSubGenerator;
569
+ let maxLength = 0;
570
+ subGenerators = subGenerators
571
+ .map((sub) => {
572
+ const subGenerator = env.get(sub.namespace);
573
+ let subcommand = deriveSubcommand(sub.namespace);
574
+ let displayName = subGenerator.displayName || subcommand;
575
+ maxLength = Math.max(displayName.length, maxLength);
576
+ return {
577
+ subcommand,
578
+ displayName,
579
+ sub,
580
+ };
581
+ })
582
+ .map(({ subcommand, displayName, sub }) => {
583
+ const transformed = {
584
+ name: `${displayName.padEnd(maxLength + 2)} [${subcommand}]`,
585
+ value: sub.namespace,
586
+ };
587
+ if (/:app$/.test(sub.namespace)) {
588
+ defaultSubGenerator = transformed;
589
+ }
590
+ return transformed;
591
+ });
592
+
593
+ // at least 1 subgenerator must be present
594
+ if (subGenerators.length >= 1) {
595
+ // by default the 1st subgenerator is used
596
+ let subGenerator = subGenerators[0].value;
597
+
598
+ // if more than 1 subgenerator is present
599
+ // ask the developer to select one!
600
+ if (subGenerators.length > 1) {
601
+ subGenerator = (
602
+ await this.prompt([
603
+ {
604
+ type: "list",
605
+ name: "subGenerator",
606
+ message: "What do you want to do?",
607
+ default: defaultSubGenerator && defaultSubGenerator.value,
608
+ choices: subGenerators,
609
+ },
610
+ ])
611
+ ).subGenerator;
612
+ }
613
+
614
+ if (this.options.verbose) {
615
+ this.log(`Calling ${chalk.red(subGenerator)}...\n \\_ in: ${generatorPath}`);
616
+ }
617
+
618
+ // finally, run the subgenerator
619
+ env.run(subGenerator, {
620
+ verbose: this.options.verbose,
621
+ embedded: true,
622
+ destinationRoot: this.destinationRoot(),
623
+ });
624
+ } else {
625
+ this.log(`The generator ${chalk.red(this.options.generator)} has no visible subgenerators!`);
626
+ }
627
+ }
661
628
  };