Skip to content

Commit afd9173

Browse files
authored
feat: support nesting of generators (#141)
Sub generators can include nested generators as a static property which are resolved and installed automatically by Easy UI5 and finally executed in a chain after the root generator and the sub generator. ```js export default class extends Generator { static displayName = "Create a new UI5 application"; static nestedGenerators = [ "wdi5", "library:app" ]; ``` Just describe the subgenerator name as you would specify it when using Easy UI5 and if you want to address a dedicated generator in the subgenerator, just use the namespace syntax defining the dedicated generator with the `:`.
1 parent 3796437 commit afd9173

File tree

3 files changed

+253
-149
lines changed

3 files changed

+253
-149
lines changed

generators/app/index.js

Lines changed: 209 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ const generatorOptions = {
134134
type: Boolean,
135135
description: "Preview the next mode to consume subgenerators from bestofui5.org",
136136
},
137+
skipNested: {
138+
type: Boolean,
139+
description: "Skips the nested generators and runs only the first subgenerator",
140+
},
137141
};
138142

139143
const generatorArgs = {
@@ -149,6 +153,7 @@ const generatorArgs = {
149153
},
150154
};
151155

156+
// The Easy UI5 Generator!
152157
export default class extends Generator {
153158
constructor(args, opts) {
154159
super(args, opts, {
@@ -239,6 +244,102 @@ export default class extends Generator {
239244
return "Easy UI5";
240245
}
241246

247+
async _getGeneratorMetadata({ env, generatorPath }) {
248+
// filter the hidden subgenerators already
249+
// -> subgenerators must be found in env as they are returned by lookup!
250+
const lookupGeneratorMeta = await env.lookup({ localOnly: true, packagePaths: generatorPath });
251+
const subGenerators = lookupGeneratorMeta.filter((sub) => {
252+
const subGenerator = env.get(sub.namespace);
253+
return !subGenerator.hidden;
254+
});
255+
return subGenerators;
256+
}
257+
258+
async _installGenerator({ octokit, generator, generatorPath }) {
259+
// lookup the default path of the generator if not set
260+
if (!generator.branch) {
261+
try {
262+
const repoInfo = await octokit.repos.get({
263+
owner: generator.org,
264+
repo: generator.name,
265+
});
266+
generator.branch = repoInfo.data.default_branch;
267+
} catch (e) {
268+
console.error(`Generator "${owner}/${repo}!${dir}${branch ? "#" + branch : ""}" not found! Run with --verbose for details!`);
269+
if (this.options.verbose) {
270+
console.error(e);
271+
}
272+
return;
273+
}
274+
}
275+
// fetch the branch to retrieve the latest commit SHA
276+
let commitSHA;
277+
try {
278+
// determine the commitSHA
279+
const reqBranch = await octokit.repos.getBranch({
280+
owner: generator.org,
281+
repo: generator.name,
282+
branch: generator.branch,
283+
});
284+
commitSHA = reqBranch.data.commit.sha;
285+
} catch (ex) {
286+
console.error(chalk.red(`Failed to retrieve the branch "${generator.branch}" for repository "${generator.name}" for "${generator.org}" organization! Run with --verbose for details!`));
287+
if (this.options.verbose) {
288+
console.error(chalk.red(ex.message));
289+
}
290+
return;
291+
}
292+
293+
if (this.options.verbose) {
294+
this.log(`Using commit ${commitSHA} from @${generator.org}/${generator.name}#${generator.branch}!`);
295+
}
296+
const shaMarker = path.join(generatorPath, `.${commitSHA}`);
297+
298+
if (fs.existsSync(generatorPath) && !this.options.skipUpdate) {
299+
// check if the SHA marker exists to know whether the generator is up-to-date or not
300+
if (this.options.forceUpdate || !fs.existsSync(shaMarker)) {
301+
if (this.options.verbose) {
302+
this.log(`Generator ${chalk.yellow(generator.name)} in "${generatorPath}" is outdated!`);
303+
}
304+
// remove if the SHA marker doesn't exist => outdated!
305+
this._showBusy(` Deleting subgenerator ${chalk.yellow(generator.name)}...`);
306+
fs.rmSync(generatorPath, { recursive: true });
307+
}
308+
}
309+
310+
// re-fetch the generator and extract into local plugin folder
311+
if (!fs.existsSync(generatorPath)) {
312+
// unzip the archive
313+
if (this.options.verbose) {
314+
this.log(`Extracting ZIP to "${generatorPath}"...`);
315+
}
316+
this._showBusy(` Downloading subgenerator ${chalk.yellow(generator.name)}...`);
317+
const reqZIPArchive = await octokit.repos.downloadZipballArchive({
318+
owner: generator.org,
319+
repo: generator.name,
320+
ref: commitSHA,
321+
});
322+
323+
this._showBusy(` Extracting subgenerator ${chalk.yellow(generator.name)}...`);
324+
const buffer = Buffer.from(new Uint8Array(reqZIPArchive.data));
325+
this._unzip(buffer, generatorPath, generator.dir);
326+
327+
// write the sha marker
328+
fs.writeFileSync(shaMarker, commitSHA);
329+
}
330+
331+
// run npm install when not embedding the generator (always for self-healing!)
332+
if (!this.options.embed) {
333+
if (this.options.verbose) {
334+
this.log("Installing the subgenerator dependencies...");
335+
}
336+
this._showBusy(` Preparing ${chalk.yellow(generator.name)}...`);
337+
await this._npmInstall(generatorPath, this.options.pluginsWithDevDeps);
338+
}
339+
340+
this._clearBusy(true);
341+
}
342+
242343
async prompting() {
243344
const home = path.join(__dirname, "..", "..");
244345
const pkgJson = JSON.parse(fs.readFileSync(path.join(home, "package.json"), "utf8"));
@@ -369,31 +470,28 @@ export default class extends Generator {
369470
// determine the generator to be used
370471
let generator;
371472

372-
// try to identify whether concrete generator is defined
373-
if (!generator) {
374-
// determine generator by ${owner}/${repo}(!${dir})? syntax, e.g.:
375-
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial
376-
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial#1.0
377-
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator
378-
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator#1.0
379-
const reGenerator = /([^\/]+)\/([^\!\#]+)(?:\!([^\#]+))?(?:\#(.+))?/;
380-
const matchGenerator = reGenerator.exec(this.options.generator);
381-
if (matchGenerator) {
382-
// derive and path the generator information from command line
383-
const [owner, repo, dir = "/generator", branch] = matchGenerator.slice(1);
384-
// the plugin path is derived from the owner, repo, dir and branch
385-
const pluginPath = `_/${owner}/${repo}${dir.replace(/[\/\\]/g, "_")}${branch ? `#${branch.replace(/[\/\\]/g, "_")}` : ""}`;
386-
generator = {
387-
org: owner,
388-
name: repo,
389-
branch,
390-
dir,
391-
pluginPath,
392-
};
393-
// log which generator is being used!
394-
if (this.options.verbose) {
395-
this.log(`Using generator ${chalk.green(`${owner}/${repo}!${dir}${branch ? "#" + branch : ""}`)}`);
396-
}
473+
// determine generator by ${owner}/${repo}(!${dir})? syntax, e.g.:
474+
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial
475+
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial#1.0
476+
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator
477+
// > yo easy-ui5 SAP-samples/ui5-typescript-tutorial\!/generator#1.0
478+
const reGenerator = /([^\/]+)\/([^\!\#]+)(?:\!([^\#]+))?(?:\#(.+))?/;
479+
const matchGenerator = reGenerator.exec(this.options.generator);
480+
if (matchGenerator) {
481+
// derive and path the generator information from command line
482+
const [owner, repo, dir = "/generator", branch] = matchGenerator.slice(1);
483+
// the plugin path is derived from the owner, repo, dir and branch
484+
const pluginPath = `_/${owner}/${repo}${dir.replace(/[\/\\]/g, "_")}${branch ? `#${branch.replace(/[\/\\]/g, "_")}` : ""}`;
485+
generator = {
486+
org: owner,
487+
name: repo,
488+
branch,
489+
dir,
490+
pluginPath,
491+
};
492+
// log which generator is being used!
493+
if (this.options.verbose) {
494+
this.log(`Using generator ${chalk.green(`${owner}/${repo}!${dir}${branch ? "#" + branch : ""}`)}`);
397495
}
398496
}
399497

@@ -527,120 +625,38 @@ export default class extends Generator {
527625
}
528626
}
529627

530-
let generatorPath = path.join(pluginsHome, generator.pluginPath || generator.name);
628+
// install the generator if not running in offline mode
629+
const generatorPath = path.join(pluginsHome, generator.pluginPath || generator.name);
531630
if (!this.options.offline) {
532-
// lookup the default path of the generator if not set
533-
if (!generator.branch) {
534-
try {
535-
const repoInfo = await octokit.repos.get({
536-
owner: generator.org,
537-
repo: generator.name,
538-
});
539-
generator.branch = repoInfo.data.default_branch;
540-
} catch (e) {
541-
console.error(`Generator "${owner}/${repo}!${dir}${branch ? "#" + branch : ""}" not found! Run with --verbose for details!`);
542-
if (this.options.verbose) {
543-
console.error(e);
544-
}
545-
return;
546-
}
547-
}
548-
// fetch the branch to retrieve the latest commit SHA
549-
let commitSHA;
550-
try {
551-
// determine the commitSHA
552-
const reqBranch = await octokit.repos.getBranch({
553-
owner: generator.org,
554-
repo: generator.name,
555-
branch: generator.branch,
556-
});
557-
commitSHA = reqBranch.data.commit.sha;
558-
} catch (ex) {
559-
console.error(chalk.red(`Failed to retrieve the branch "${generator.branch}" for repository "${generator.name}" for "${generator.org}" organization! Run with --verbose for details!`));
560-
if (this.options.verbose) {
561-
console.error(chalk.red(ex.message));
562-
}
563-
return;
564-
}
565-
566-
if (this.options.verbose) {
567-
this.log(`Using commit ${commitSHA} from @${generator.org}/${generator.name}#${generator.branch}!`);
568-
}
569-
const shaMarker = path.join(generatorPath, `.${commitSHA}`);
570-
571-
if (fs.existsSync(generatorPath) && !this.options.skipUpdate) {
572-
// check if the SHA marker exists to know whether the generator is up-to-date or not
573-
if (this.options.forceUpdate || !fs.existsSync(shaMarker)) {
574-
if (this.options.verbose) {
575-
this.log(`Generator ${chalk.yellow(generator.name)} in "${generatorPath}" is outdated!`);
576-
}
577-
// remove if the SHA marker doesn't exist => outdated!
578-
this._showBusy(` Deleting subgenerator ${chalk.yellow(generator.name)}...`);
579-
fs.rmSync(generatorPath, { recursive: true });
580-
}
581-
}
582-
583-
// re-fetch the generator and extract into local plugin folder
584-
if (!fs.existsSync(generatorPath)) {
585-
// unzip the archive
586-
if (this.options.verbose) {
587-
this.log(`Extracting ZIP to "${generatorPath}"...`);
588-
}
589-
this._showBusy(` Downloading subgenerator ${chalk.yellow(generator.name)}...`);
590-
const reqZIPArchive = await octokit.repos.downloadZipballArchive({
591-
owner: generator.org,
592-
repo: generator.name,
593-
ref: commitSHA,
594-
});
595-
596-
this._showBusy(` Extracting subgenerator ${chalk.yellow(generator.name)}...`);
597-
const buffer = Buffer.from(new Uint8Array(reqZIPArchive.data));
598-
this._unzip(buffer, generatorPath, generator.dir);
599-
600-
// write the sha marker
601-
fs.writeFileSync(shaMarker, commitSHA);
602-
}
603-
604-
// only when embedding we clear the busy state as otherwise
605-
// the npm install will immediately again show the busy state
606-
if (this.options.embed) {
607-
this._clearBusy(true);
608-
}
631+
await this._installGenerator({ octokit, generator, generatorPath });
609632
}
610633

611634
// do not execute the plugin generator during the setup/embed mode
612635
if (!this.options.embed) {
613636
// filter the local options and the help command
614637
const opts = Object.keys(this._options).filter((optionName) => !(generatorOptions.hasOwnProperty(optionName) || optionName === "help"));
615638

616-
// run npm install (always for self-healing!)
617-
if (this.options.verbose) {
618-
this.log("Installing the subgenerator dependencies...");
619-
}
620-
this._showBusy(` Preparing ${chalk.yellow(generator.name)}...`);
621-
await this._npmInstall(generatorPath, this.options.pluginsWithDevDeps);
622-
this._clearBusy(true);
623-
624639
// create the env for the plugin generator
625640
let env = this.env; // in case of Yeoman UI the env is injected!
626641
if (!env) {
627642
const yeoman = require("yeoman-environment");
628643
env = yeoman.createEnv(this.args, opts);
629644
}
630645

631-
// helper to derive the subcommand
632-
function deriveSubcommand(namespace) {
633-
const match = namespace.match(/[^:]+:(.+)/);
634-
return match ? match[1] : namespace;
646+
// read the generator metadata
647+
let subGenerators = await this._getGeneratorMetadata({ env, generatorPath });
648+
649+
// helper to derive the generator from the namespace
650+
function deriveGenerator(namespace, defaultValue) {
651+
const match = namespace.match(/([^:]+):.+/);
652+
return match ? match[1] : defaultValue === undefined ? namespace : defaultValue;
635653
}
636654

637-
// filter the hidden subgenerators already
638-
// -> subgenerators must be found in env as they are returned by lookup!
639-
const lookupGeneratorMeta = await env.lookup({ localOnly: true, packagePaths: generatorPath });
640-
let subGenerators = lookupGeneratorMeta.filter((sub) => {
641-
const subGenerator = env.get(sub.namespace);
642-
return !subGenerator.hidden;
643-
});
655+
// helper to derive the subcommand from the namespace
656+
function deriveSubcommand(namespace, defaultValue) {
657+
const match = namespace.match(/^[^:]+:(.+)$/);
658+
return match ? match[1] : defaultValue === undefined ? namespace : defaultValue;
659+
}
644660

645661
// list the available subgenerators in the console (as help)
646662
if (this.options.list) {
@@ -726,16 +742,79 @@ export default class extends Generator {
726742
).subGenerator;
727743
}
728744

729-
if (this.options.verbose) {
730-
this.log(`Calling ${chalk.red(subGenerator)}...\n \\_ in "${generatorPath}"`);
745+
// determine the list of subgenerators to be executed
746+
const subGensToRun = [subGenerator];
747+
748+
// method to resolve nested generators (only once!)
749+
const resolved = [];
750+
const resolveNestedGenerator = async (generatorToResolve) => {
751+
const constructor = await env.get(generatorToResolve);
752+
await Promise.all(
753+
constructor.nestedGenerators?.map(async (nestedGenerator) => {
754+
const theNestedGenerator = deriveGenerator(nestedGenerator);
755+
if (resolved.indexOf(theNestedGenerator) === -1) {
756+
resolved.push(theNestedGenerator);
757+
const nestedGeneratorInfo = availGenerators.find((repo) => repo.subGeneratorName === theNestedGenerator);
758+
const nestedGeneratorPath = path.join(pluginsHome, nestedGeneratorInfo.pluginPath || nestedGeneratorInfo.name);
759+
await this._installGenerator({ octokit, generator: nestedGeneratorInfo, generatorPath: nestedGeneratorPath });
760+
const nestedGens = await this._getGeneratorMetadata({ env, generatorPath: nestedGeneratorPath });
761+
const subcommand = deriveSubcommand(nestedGenerator, "");
762+
const theNestedGen = nestedGens.filter((nested) => {
763+
const nestedSubcommand = deriveSubcommand(nested.namespace, "");
764+
return subcommand ? nestedSubcommand === subcommand : !nestedSubcommand;
765+
})?.[0];
766+
if (theNestedGen) {
767+
subGensToRun.push(theNestedGen.namespace);
768+
await resolveNestedGenerator(theNestedGen.namespace);
769+
} else {
770+
this.log(`The nested generator "${nestedGeneratorInfo.org}/${nestedGeneratorInfo.name}" has no subgenerator "${subcommand || "default"}"! Ignoring execution...`);
771+
}
772+
}
773+
}) || []
774+
);
775+
};
776+
777+
// only resolve nested generators when they should not be skipped
778+
if (!this.options.skipNested) {
779+
await resolveNestedGenerator(subGenerator);
731780
}
732781

733-
// finally, run the subgenerator
734-
env.run(subGenerator, {
735-
verbose: this.options.verbose,
736-
embedded: true,
737-
destinationRoot: this.destinationRoot(),
738-
});
782+
// intercept the environments runGenerator method to determine
783+
// and forward the destinationRoot between the generator executions
784+
const runGenerator = env.runGenerator;
785+
let cwd;
786+
env.runGenerator = async function (gen) {
787+
if (cwd) {
788+
// apply the cwd to the next gen
789+
gen.destinationRoot(cwd);
790+
}
791+
return runGenerator.apply(this, arguments).then((retval) => {
792+
// store the cwd from the current gen
793+
cwd = gen.destinationRoot();
794+
return retval;
795+
});
796+
};
797+
798+
// chain the execution of the generators
799+
let chain = Promise.resolve();
800+
for (const subGen of subGensToRun) {
801+
chain = chain.then(
802+
function () {
803+
// we need to use env.run and not composeWith
804+
// to ensure that subgenerators can have different
805+
// dependencies than the root generator
806+
return env.run(subGen, {
807+
verbose: this.options.verbose,
808+
embedded: true,
809+
destinationRoot: this.destinationRoot(),
810+
});
811+
}.bind(this)
812+
);
813+
}
814+
815+
if (this.options.verbose) {
816+
this.log(`Running generators in "${generatorPath}"...`);
817+
}
739818
} else {
740819
this.log(`The generator ${chalk.red(this.options.generator)} has no visible subgenerators!`);
741820
}

0 commit comments

Comments
 (0)