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