Skip to content

Commit

Permalink
feat: support nesting of generators
Browse files Browse the repository at this point in the history
  • Loading branch information
petermuessig committed Jan 5, 2024
1 parent 3796437 commit 58effea
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 157 deletions.
346 changes: 208 additions & 138 deletions generators/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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 : ""}`)}`);
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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!`);
}
Expand Down
Loading

0 comments on commit 58effea

Please sign in to comment.