diff --git a/generators/app/index.js b/generators/app/index.js index 6dcb8f1..d5a0ade 100644 --- a/generators/app/index.js +++ b/generators/app/index.js @@ -149,6 +149,29 @@ const generatorArgs = { }, }; +// helper to delegate the destination root to the next +// composed generator in the loop (to ensure the same root) +class DestinationRootDelegator extends Generator { + static composedGenerators; + constructor(args, options) { + super(args, options); + } + _currentGen() { + return this.options.current; + } + _nextGen() { + const i = DestinationRootDelegator.composedGenerators.indexOf(this._currentGen()); + return i !== -1 ? DestinationRootDelegator.composedGenerators[i + 1] : undefined; + } + _delegateDestinationRoot() { + this._nextGen()?.destinationRoot(this._currentGen().destinationRoot()); + } +} +["initializing", "configuring", "prompting", "writing", "conflicts", "install", "end"].forEach((prio) => { + DestinationRootDelegator.prototype[prio] = DestinationRootDelegator.prototype._delegateDestinationRoot; +}); + +// The Easy UI5 Generator! export default class extends Generator { constructor(args, opts) { super(args, opts, { @@ -239,6 +262,102 @@ export default class extends Generator { return "Easy UI5"; } + async _getGeneratorMetadata({ env, generatorPath }) { + // filter the hidden subgenerators already + // -> subgenerators must be found in env as they are returned by lookup! + const lookupGeneratorMeta = await env.lookup({ localOnly: true, packagePaths: generatorPath }); + const subGenerators = lookupGeneratorMeta.filter((sub) => { + const subGenerator = env.get(sub.namespace); + return !subGenerator.hidden; + }); + return subGenerators; + } + + async _installGenerator({ octokit, generator, generatorPath }) { + // lookup the default path of the generator if not set + if (!generator.branch) { + try { + const repoInfo = await octokit.repos.get({ + owner: generator.org, + repo: generator.name, + }); + generator.branch = repoInfo.data.default_branch; + } catch (e) { + console.error(`Generator "${owner}/${repo}!${dir}${branch ? "#" + branch : ""}" not found! Run with --verbose for details!`); + if (this.options.verbose) { + console.error(e); + } + return; + } + } + // fetch the branch to retrieve the latest commit SHA + let commitSHA; + try { + // determine the commitSHA + const reqBranch = await octokit.repos.getBranch({ + owner: generator.org, + repo: generator.name, + branch: generator.branch, + }); + commitSHA = reqBranch.data.commit.sha; + } catch (ex) { + console.error(chalk.red(`Failed to retrieve the branch "${generator.branch}" for repository "${generator.name}" for "${generator.org}" organization! Run with --verbose for details!`)); + if (this.options.verbose) { + console.error(chalk.red(ex.message)); + } + return; + } + + if (this.options.verbose) { + this.log(`Using commit ${commitSHA} from @${generator.org}/${generator.name}#${generator.branch}!`); + } + const shaMarker = path.join(generatorPath, `.${commitSHA}`); + + if (fs.existsSync(generatorPath) && !this.options.skipUpdate) { + // check if the SHA marker exists to know whether the generator is up-to-date or not + if (this.options.forceUpdate || !fs.existsSync(shaMarker)) { + if (this.options.verbose) { + this.log(`Generator ${chalk.yellow(generator.name)} in "${generatorPath}" is outdated!`); + } + // remove if the SHA marker doesn't exist => outdated! + this._showBusy(` Deleting subgenerator ${chalk.yellow(generator.name)}...`); + fs.rmSync(generatorPath, { recursive: true }); + } + } + + // re-fetch the generator and extract into local plugin folder + if (!fs.existsSync(generatorPath)) { + // unzip the archive + if (this.options.verbose) { + this.log(`Extracting ZIP to "${generatorPath}"...`); + } + this._showBusy(` Downloading subgenerator ${chalk.yellow(generator.name)}...`); + const reqZIPArchive = await octokit.repos.downloadZipballArchive({ + owner: generator.org, + repo: generator.name, + ref: commitSHA, + }); + + this._showBusy(` Extracting subgenerator ${chalk.yellow(generator.name)}...`); + const buffer = Buffer.from(new Uint8Array(reqZIPArchive.data)); + this._unzip(buffer, generatorPath, generator.dir); + + // write the sha marker + fs.writeFileSync(shaMarker, commitSHA); + } + + // run npm install when not embedding the generator (always for self-healing!) + if (!this.options.embed) { + if (this.options.verbose) { + this.log("Installing the subgenerator dependencies..."); + } + this._showBusy(` Preparing ${chalk.yellow(generator.name)}...`); + await this._npmInstall(generatorPath, this.options.pluginsWithDevDeps); + } + + this._clearBusy(true); + } + async prompting() { const home = path.join(__dirname, "..", ".."); const pkgJson = JSON.parse(fs.readFileSync(path.join(home, "package.json"), "utf8")); @@ -369,31 +488,28 @@ export default class extends Generator { // determine the generator to be used let generator; - // try to identify whether concrete generator is defined - if (!generator) { - // determine generator by ${owner}/${repo}(!${dir})? syntax, e.g.: - // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial - // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial#1.0 - // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator - // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator#1.0 - const reGenerator = /([^\/]+)\/([^\!\#]+)(?:\!([^\#]+))?(?:\#(.+))?/; - const matchGenerator = reGenerator.exec(this.options.generator); - if (matchGenerator) { - // derive and path the generator information from command line - const [owner, repo, dir = "/generator", branch] = matchGenerator.slice(1); - // the plugin path is derived from the owner, repo, dir and branch - const pluginPath = `_/${owner}/${repo}${dir.replace(/[\/\\]/g, "_")}${branch ? `#${branch.replace(/[\/\\]/g, "_")}` : ""}`; - generator = { - org: owner, - name: repo, - branch, - dir, - pluginPath, - }; - // log which generator is being used! - if (this.options.verbose) { - this.log(`Using generator ${chalk.green(`${owner}/${repo}!${dir}${branch ? "#" + branch : ""}`)}`); - } + // determine generator by ${owner}/${repo}(!${dir})? syntax, e.g.: + // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial + // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial#1.0 + // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator + // > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator#1.0 + const reGenerator = /([^\/]+)\/([^\!\#]+)(?:\!([^\#]+))?(?:\#(.+))?/; + const matchGenerator = reGenerator.exec(this.options.generator); + if (matchGenerator) { + // derive and path the generator information from command line + const [owner, repo, dir = "/generator", branch] = matchGenerator.slice(1); + // the plugin path is derived from the owner, repo, dir and branch + const pluginPath = `_/${owner}/${repo}${dir.replace(/[\/\\]/g, "_")}${branch ? `#${branch.replace(/[\/\\]/g, "_")}` : ""}`; + generator = { + org: owner, + name: repo, + branch, + dir, + pluginPath, + }; + // log which generator is being used! + if (this.options.verbose) { + this.log(`Using generator ${chalk.green(`${owner}/${repo}!${dir}${branch ? "#" + branch : ""}`)}`); } } @@ -527,121 +643,37 @@ export default class extends Generator { } } - let generatorPath = path.join(pluginsHome, generator.pluginPath || generator.name); - if (!this.options.offline) { - // lookup the default path of the generator if not set - if (!generator.branch) { - try { - const repoInfo = await octokit.repos.get({ - owner: generator.org, - repo: generator.name, - }); - generator.branch = repoInfo.data.default_branch; - } catch (e) { - console.error(`Generator "${owner}/${repo}!${dir}${branch ? "#" + branch : ""}" not found! Run with --verbose for details!`); - if (this.options.verbose) { - console.error(e); - } - return; - } - } - // fetch the branch to retrieve the latest commit SHA - let commitSHA; - try { - // determine the commitSHA - const reqBranch = await octokit.repos.getBranch({ - owner: generator.org, - repo: generator.name, - branch: generator.branch, - }); - commitSHA = reqBranch.data.commit.sha; - } catch (ex) { - console.error(chalk.red(`Failed to retrieve the branch "${generator.branch}" for repository "${generator.name}" for "${generator.org}" organization! Run with --verbose for details!`)); - if (this.options.verbose) { - console.error(chalk.red(ex.message)); - } - return; - } - - if (this.options.verbose) { - this.log(`Using commit ${commitSHA} from @${generator.org}/${generator.name}#${generator.branch}!`); - } - const shaMarker = path.join(generatorPath, `.${commitSHA}`); - - if (fs.existsSync(generatorPath) && !this.options.skipUpdate) { - // check if the SHA marker exists to know whether the generator is up-to-date or not - if (this.options.forceUpdate || !fs.existsSync(shaMarker)) { - if (this.options.verbose) { - this.log(`Generator ${chalk.yellow(generator.name)} in "${generatorPath}" is outdated!`); - } - // remove if the SHA marker doesn't exist => outdated! - this._showBusy(` Deleting subgenerator ${chalk.yellow(generator.name)}...`); - fs.rmSync(generatorPath, { recursive: true }); - } - } - - // re-fetch the generator and extract into local plugin folder - if (!fs.existsSync(generatorPath)) { - // unzip the archive - if (this.options.verbose) { - this.log(`Extracting ZIP to "${generatorPath}"...`); - } - this._showBusy(` Downloading subgenerator ${chalk.yellow(generator.name)}...`); - const reqZIPArchive = await octokit.repos.downloadZipballArchive({ - owner: generator.org, - repo: generator.name, - ref: commitSHA, - }); - - this._showBusy(` Extracting subgenerator ${chalk.yellow(generator.name)}...`); - const buffer = Buffer.from(new Uint8Array(reqZIPArchive.data)); - this._unzip(buffer, generatorPath, generator.dir); + // filter the local options and the help command + const opts = Object.keys(this._options).filter((optionName) => !(generatorOptions.hasOwnProperty(optionName) || optionName === "help")); - // write the sha marker - fs.writeFileSync(shaMarker, commitSHA); - } + // create the env for the plugin generator + let env = this.env; // in case of Yeoman UI the env is injected! + if (!env) { + const yeoman = require("yeoman-environment"); + env = yeoman.createEnv(this.args, opts); + } - // only when embedding we clear the busy state as otherwise - // the npm install will immediately again show the busy state - if (this.options.embed) { - this._clearBusy(true); - } + // install the generator if not running in offline mode + const generatorPath = path.join(pluginsHome, generator.pluginPath || generator.name); + if (!this.options.offline) { + await this._installGenerator({ octokit, generator, generatorPath }); } + let subGenerators = await this._getGeneratorMetadata({ env, generatorPath }); // do not execute the plugin generator during the setup/embed mode if (!this.options.embed) { - // filter the local options and the help command - const opts = Object.keys(this._options).filter((optionName) => !(generatorOptions.hasOwnProperty(optionName) || optionName === "help")); - - // run npm install (always for self-healing!) - if (this.options.verbose) { - this.log("Installing the subgenerator dependencies..."); + // helper to derive the generator from the namespace + function deriveGenerator(namespace, defaultValue) { + const match = namespace.match(/([^:]+):.+/); + return match ? match[1] : defaultValue === undefined ? namespace : defaultValue; } - this._showBusy(` Preparing ${chalk.yellow(generator.name)}...`); - await this._npmInstall(generatorPath, this.options.pluginsWithDevDeps); - this._clearBusy(true); - // create the env for the plugin generator - let env = this.env; // in case of Yeoman UI the env is injected! - if (!env) { - const yeoman = require("yeoman-environment"); - env = yeoman.createEnv(this.args, opts); + // helper to derive the subcommand from the namespace + function deriveSubcommand(namespace, defaultValue) { + const match = namespace.match(/^[^:]+:(.+)$/); + return match ? match[1] : defaultValue === undefined ? namespace : defaultValue; } - // helper to derive the subcommand - function deriveSubcommand(namespace) { - const match = namespace.match(/[^:]+:(.+)/); - return match ? match[1] : namespace; - } - - // filter the hidden subgenerators already - // -> subgenerators must be found in env as they are returned by lookup! - const lookupGeneratorMeta = await env.lookup({ localOnly: true, packagePaths: generatorPath }); - let subGenerators = lookupGeneratorMeta.filter((sub) => { - const subGenerator = env.get(sub.namespace); - return !subGenerator.hidden; - }); - // list the available subgenerators in the console (as help) if (this.options.list) { let maxLength = 0; @@ -726,16 +758,54 @@ export default class extends Generator { ).subGenerator; } + // determine the list of subgenerators to be executed + const subGensToRun = [subGenerator]; + + // method to resolve nested generators (only once!) + const resolved = []; + const resolveNestedGenerator = async (generatorToResolve) => { + const constructor = await env.get(generatorToResolve); + await Promise.all( + constructor.nestedGenerators?.map(async (nestedGenerator) => { + const theNestedGenerator = deriveGenerator(nestedGenerator); + if (resolved.indexOf(theNestedGenerator) === -1) { + resolved.push(theNestedGenerator); + const nestedGeneratorInfo = availGenerators.find((repo) => repo.subGeneratorName === theNestedGenerator); + const nestedGeneratorPath = path.join(pluginsHome, nestedGeneratorInfo.pluginPath || nestedGeneratorInfo.name); + await this._installGenerator({ octokit, generator: nestedGeneratorInfo, generatorPath: nestedGeneratorPath }); + const nestedGens = this._getGeneratorMetadata({ env, generatorPath: nestedGeneratorPath }); + const subcommand = deriveSubcommand(nestedGenerator, ""); + const theNestedGen = nestedGens.filter((nested) => (subcommand ? nested.subcommand === subcommand : !nested.subcommand))?.[0]; + subGensToRun.push(theNestedGen.namespace); + await resolveNestedGenerator(theNestedGen.namespace); + } + }) || [] + ); + }; + + // resolve all nested generators first + await resolveNestedGenerator(subGenerator); + + // chain the execution of the generators + DestinationRootDelegator.composedGenerators = await Promise.all( + subGensToRun.map(async (subGenToRun) => { + if (this.options.verbose) { + this.log(`Composing with ${chalk.red(subGenToRun)}...`); + } + // compose with the subgenerator + const newGen = await this.composeWith(subGenToRun, { + verbose: this.options.verbose, + embedded: true, + }); + // use the delegator to push the destination root to the next generator + this.composeWith({ Generator: DestinationRootDelegator, path: "./" }, { current: newGen }); + return newGen; + }) + ); + if (this.options.verbose) { - this.log(`Calling ${chalk.red(subGenerator)}...\n \\_ in "${generatorPath}"`); + this.log(`Running generators in "${generatorPath}"...`); } - - // finally, run the subgenerator - env.run(subGenerator, { - verbose: this.options.verbose, - embedded: true, - destinationRoot: this.destinationRoot(), - }); } else { this.log(`The generator ${chalk.red(this.options.generator)} has no visible subgenerators!`); } diff --git a/package-lock.json b/package-lock.json index c795d8c..6d95344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "generator-easy-ui5", - "version": "3.7.0", + "version": "3.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "generator-easy-ui5", - "version": "3.7.0", + "version": "3.8.0", "license": "Apache-2.0", "dependencies": { "@octokit/plugin-throttling": "^8.1.3", @@ -2341,9 +2341,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", - "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "engines": { "node": ">=6" }, @@ -10693,18 +10693,18 @@ } }, "node_modules/yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" @@ -10758,6 +10758,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yargs/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -13969,9 +13983,9 @@ } }, "cli-spinners": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", - "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==" + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" }, "cli-table": { "version": "0.3.11", @@ -20148,20 +20162,31 @@ "dev": true }, "yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/package.json b/package.json index e58a79d..ea7dcdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "generator-easy-ui5", - "version": "3.7.0", + "version": "3.8.0", "description": "Generator for UI5-based project", "main": "generators/app/index.js", "type": "module",