gen-jhipster
Version:
VHipster - Spring Boot + Angular/React/Vue in one handy generator
706 lines (705 loc) • 30.7 kB
JavaScript
/**
* Copyright 2013-2026 the original author or authors from the JHipster project.
*
* This file is part of the JHipster project, see https://www.jhipster.tech/
* for more information.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import assert from 'node:assert';
import fs, { existsSync, readFileSync, statSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import path, { relative } from 'node:path';
import chalk from 'chalk';
import { execaCommandSync } from 'execa';
import { union } from 'lodash-es';
import semver, { lt as semverLessThan } from 'semver';
import { packageJson } from "../../lib/index.js";
import { packageNameToNamespace } from "../../lib/utils/index.js";
import CoreGenerator from "../base-core/index.js";
import { PRIORITY_NAMES } from "../base-core/priorities.js";
import { GENERATOR_JHIPSTER } from "../generator-constants.js";
import { mergeBlueprints, normalizeBlueprintName, parseBlueprints } from "./internal/index.js";
import { CONTEXT_DATA_BLUEPRINTS_TO_COMPOSE, CONTEXT_DATA_EXISTING_PROJECT, CONTEXT_DATA_REPRODUCIBLE_TIMESTAMP, LOCAL_BLUEPRINT_PACKAGE_NAMESPACE, formatDateForChangelog, } from "./support/index.js";
const { WRITING } = PRIORITY_NAMES;
/**
* Base class that contains blueprints support.
* Provides built-in state support with control object.
*/
export default class BaseGenerator extends CoreGenerator {
fromBlueprint;
sbsBlueprint;
delegateToBlueprint = false;
blueprintConfig;
jhipsterContext;
constructor(args, options, features) {
const { jhipsterContext, ...opts } = options ?? {};
super(args, opts, { blueprintSupport: true, ...features });
if (this.options.help) {
return;
}
const { sbsBlueprint = false, checkBlueprint, jhipsterBootstrap = this._namespace !== 'jhipster:project-name' &&
!this._namespace.split(':')[1]?.startsWith('bootstrap') &&
!this._namespace.endsWith(':bootstrap'), } = this.features;
this.sbsBlueprint = sbsBlueprint;
this.fromBlueprint = !['generator-jhipster', packageJson.name].includes(this.rootGeneratorName());
if (this.fromBlueprint) {
this.blueprintStorage = this._getStorage();
this.blueprintConfig = this.blueprintStorage.createProxy();
// jhipsterContext is the original generator
this.jhipsterContext = jhipsterContext;
if (checkBlueprint && !this.jhipsterContext) {
throw new Error(`This is a JHipster blueprint and should be used only like ${chalk.yellow(`jhipster --blueprints ${this.options.namespace.split(':')[0]}`)}`);
}
try {
// Fallback to the original generator if the file does not exists in the blueprint.
const blueprintedTemplatePath = this.jhipsterTemplatePath();
if (!this.jhipsterTemplatesFolders.includes(blueprintedTemplatePath)) {
this.jhipsterTemplatesFolders.push(blueprintedTemplatePath);
}
}
catch (error) {
this.log.warn('Error adding current blueprint templates as alternative for JHipster templates.');
this.log.log(error);
}
}
if (jhipsterBootstrap) {
// jhipster:bootstrap is always required. Run it once the environment starts.
this.env.queueTask('environment:run', async () => this.composeWithJHipster('bootstrap').then(), {
once: 'queueJhipsterBootstrap',
startQueue: false,
});
}
this.on('before:queueOwnTasks', () => {
const { storeBlueprintVersion, storeJHipsterVersion, queueCommandTasks = true } = this.features;
if (this.fromBlueprint && storeBlueprintVersion && !this.options.reproducibleTests) {
try {
const blueprintPackageJson = JSON.parse(readFileSync(this._meta.packagePath, 'utf8'));
this.blueprintConfig.blueprintVersion = blueprintPackageJson.version;
}
catch {
this.log(`Could not retrieve version of blueprint '${this.options.namespace}'`);
}
}
if (!this.fromBlueprint && !this.delegateToBlueprint && storeJHipsterVersion && !this.options.reproducibleTests) {
this.jhipsterConfig.jhipsterVersion = packageJson.version;
}
if ((this.fromBlueprint || !this.delegateToBlueprint) && queueCommandTasks) {
this._queueCurrentJHipsterCommandTasks();
}
});
}
/**
* Filter generator's tasks in case the blueprint should be responsible on queueing those tasks.
*/
delegateTasksToBlueprint(tasksGetter) {
return this.delegateToBlueprint ? {} : tasksGetter();
}
get #control() {
const generator = this;
return this.getContextData('jhipster:control', {
factory: () => {
let jhipsterOldVersion;
let environmentHasDockerCompose;
const customizeRemoveFiles = [];
return {
get existingProject() {
try {
return generator.getContextData(CONTEXT_DATA_EXISTING_PROJECT);
}
catch {
return false;
}
},
get jhipsterOldVersion() {
if (jhipsterOldVersion === undefined) {
jhipsterOldVersion = existsSync(generator.config.path)
? (JSON.parse(readFileSync(generator.config.path, 'utf-8').toString())[GENERATOR_JHIPSTER]?.jhipsterVersion ?? null)
: null;
}
return jhipsterOldVersion;
},
get environmentHasDockerCompose() {
if (environmentHasDockerCompose === undefined) {
const commandReturn = execaCommandSync('docker compose version', { reject: false, stdio: 'pipe' });
environmentHasDockerCompose = !commandReturn || !commandReturn.failed; // TODO looks to be a bug on ARM MaCs and execaCommandSync, does not return anything, assuming mac users are smart and install docker.
}
return environmentHasDockerCompose;
},
customizeRemoveFiles,
isJhipsterVersionLessThan(version) {
const jhipsterOldVersion = this.jhipsterOldVersion;
return jhipsterOldVersion ? semverLessThan(jhipsterOldVersion, version) : false;
},
async removeFiles(assertions, ...files) {
const versions = typeof assertions === 'string' ? { removedInVersion: undefined, oldVersion: undefined } : assertions;
if (typeof assertions === 'string') {
files = [assertions, ...files];
}
for (const customize of this.customizeRemoveFiles) {
files = files.map(customize).filter(Boolean);
}
const { removedInVersion, oldVersion = this.jhipsterOldVersion } = versions;
if (removedInVersion && oldVersion && !semverLessThan(oldVersion, removedInVersion)) {
return;
}
const absolutePaths = files.map(file => generator.destinationPath(file));
// Delete from memory fs to keep updated.
generator.fs.delete(absolutePaths);
await Promise.all(absolutePaths.map(async (file) => {
const relativePath = relative(generator.env.logCwd, file);
try {
if (statSync(file).isFile()) {
generator.log.info(`Removing legacy file ${relativePath}`);
await rm(file, { force: true });
}
}
catch {
generator.log.info(`Could not remove legacy file ${relativePath}`);
}
}));
},
async cleanupFiles(oldVersionOrCleanup, cleanup) {
if (!this.jhipsterOldVersion)
return;
let oldVersion;
if (typeof oldVersionOrCleanup === 'string') {
oldVersion = oldVersionOrCleanup;
assert(cleanup, 'cleanupFiles requires cleanup object');
}
else {
cleanup = oldVersionOrCleanup;
oldVersion = this.jhipsterOldVersion;
}
await Promise.all(Object.entries(cleanup).map(async ([version, files]) => {
const stringFiles = [];
for (const file of files) {
if (Array.isArray(file)) {
const [condition, ...fileParts] = file;
if (condition) {
stringFiles.push(...fileParts);
}
}
else {
stringFiles.push(file);
}
}
await this.removeFiles({ oldVersion, removedInVersion: version }, ...stringFiles);
}));
},
};
},
});
}
/**
* Generate a timestamp to be used by Liquibase changelogs.
*/
nextTimestamp() {
const reproducible = Boolean(this.options.reproducible);
// Use started counter or use stored creationTimestamp if creationTimestamp option is passed
const creationTimestamp = this.options.creationTimestamp ? this.config.get('creationTimestamp') : undefined;
let now = new Date();
// Milliseconds is ignored for changelogDate.
now.setMilliseconds(0);
// Run reproducible timestamp when regenerating the project with reproducible option or a specific timestamp.
if (reproducible || creationTimestamp) {
now = this.getContextData(CONTEXT_DATA_REPRODUCIBLE_TIMESTAMP, {
factory: () => {
const newCreationTimestamp = creationTimestamp ?? this.config.get('creationTimestamp');
const newDate = newCreationTimestamp ? new Date(newCreationTimestamp) : now;
newDate.setMilliseconds(0);
return newDate;
},
});
now.setMinutes(now.getMinutes() + 1);
this.getContextData(CONTEXT_DATA_REPRODUCIBLE_TIMESTAMP, { replacement: now });
// Reproducible build can create future timestamp, save it.
const lastLiquibaseTimestamp = this.jhipsterConfig.lastLiquibaseTimestamp;
if (!lastLiquibaseTimestamp || now.getTime() > lastLiquibaseTimestamp) {
this.config.set('lastLiquibaseTimestamp', now.getTime());
}
}
else {
// Get and store lastLiquibaseTimestamp, a future timestamp can be used
const lastLiquibaseTimestamp = this.jhipsterConfig.lastLiquibaseTimestamp;
if (lastLiquibaseTimestamp) {
const lastTimestampDate = new Date(lastLiquibaseTimestamp);
if (lastTimestampDate >= now) {
now = lastTimestampDate;
now.setSeconds(now.getSeconds() + 1);
now.setMilliseconds(0);
}
}
this.jhipsterConfig.lastLiquibaseTimestamp = now.getTime();
}
return formatDateForChangelog(now);
}
/**
* Get arguments for the priority
*/
getArgsForPriority(priorityName) {
const [firstArg] = super.getArgsForPriority(priorityName);
const control = this.#control;
if (priorityName === WRITING && existsSync(this.config.path)) {
try {
const oldConfig = JSON.parse(readFileSync(this.config.path).toString())[GENERATOR_JHIPSTER];
const newConfig = this.config.getAll();
const keys = [...new Set([...Object.keys(oldConfig), ...Object.keys(newConfig)])];
const configChanges = Object.fromEntries(keys
.filter(key => Array.isArray(newConfig[key])
? newConfig[key].length === oldConfig[key].length &&
newConfig[key].find((element, index) => element !== oldConfig[key][index])
: newConfig[key] !== oldConfig[key])
.map(key => [key, { newValue: newConfig[key], oldValue: oldConfig[key] }]));
return [{ ...firstArg, control, configChanges }];
}
catch {
// Fail to parse
}
}
return [{ ...firstArg, control }];
}
/**
* Check if the generator should ask for prompts.
*/
shouldAskForPrompts({ control }) {
if (!control)
throw new Error(`Control object not found in ${this.options.namespace}`);
return !control.existingProject || this.options.askAnswered === true;
}
/**
* Priority API stub for blueprints.
*
* Initializing priority is used to show logo and tasks related to preparing for prompts, like loading constants.
*/
get initializing() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asInitializingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Prompting priority is used to prompt users for configuration values.
*/
get prompting() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asPromptingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Configuring priority is used to customize and validate the configuration.
*/
get configuring() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asConfiguringTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Composing should be used to compose with others generators.
*/
get composing() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asComposingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* ComposingComponent priority should be used to handle component configuration order.
*/
get composingComponent() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asComposingComponentTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Loading should be used to load application configuration from jhipster configuration.
* Before this priority the configuration should be considered dirty, while each generator configures itself at configuring priority, another generator composed at composing priority can still change it.
*/
get loading() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asLoadingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Preparing should be used to generate derived properties.
*/
get preparing() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asPreparingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Preparing should be used to generate derived properties.
*/
get postPreparing() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asPostPreparingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Default priority should used as misc customizations.
*/
get default() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asDefaultTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Writing priority should used to write files.
*/
get writing() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asWritingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* PostWriting priority should used to customize files.
*/
get postWriting() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asPostWritingTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* Install priority should used to prepare the project.
*/
get install() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asInstallTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* PostWriting priority should used to customize files.
*/
get postInstall() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asPostInstallTaskGroup(taskGroup) {
return taskGroup;
}
/**
* Priority API stub for blueprints.
*
* End priority should used to say good bye and print instructions.
*/
get end() {
return {};
}
/**
* Utility method to get typed objects for autocomplete.
*/
asEndTaskGroup(taskGroup) {
return taskGroup;
}
/**
* @protected
* Composes with blueprint generators, if any.
*/
async composeWithBlueprints() {
if (this.fromBlueprint) {
throw new Error('Only the main generator can compose with blueprints');
}
const namespace = this._namespace;
if (!namespace?.startsWith('jhipster:')) {
throw new Error(`Generator is not blueprintable ${namespace}`);
}
const subGen = namespace.slice('jhipster:'.length);
this.delegateToBlueprint = false;
if (this.options.disableBlueprints) {
return [];
}
let blueprints = await this.#configureBlueprints();
if (this.options.composeWithLocalBlueprint) {
blueprints = blueprints.concat('@jhipster/local');
}
const composedBlueprints = [];
for (const blueprintName of blueprints) {
const blueprintGenerator = await this.#composeBlueprint(blueprintName, subGen);
let blueprintCommand;
if (blueprintGenerator) {
composedBlueprints.push(blueprintGenerator);
if (blueprintGenerator.sbsBlueprint) {
// If sbsBlueprint, add templatePath to the original generator templatesFolder.
this.jhipsterTemplatesFolders.unshift(blueprintGenerator.templatePath());
}
else {
// If the blueprints does not sets sbsBlueprint property, ignore normal workflow.
this.delegateToBlueprint = true;
this.#checkBlueprintImplementsPriorities(blueprintGenerator);
}
const blueprintModule = await blueprintGenerator._meta?.importModule?.();
blueprintCommand = blueprintModule?.command;
}
else {
const generatorName = packageNameToNamespace(normalizeBlueprintName(blueprintName));
const generatorNamespace = `${generatorName}:${subGen}`;
const blueprintMeta = this.env.findMeta(generatorNamespace);
const blueprintModule = await blueprintMeta?.importModule?.();
blueprintCommand = blueprintModule?.command;
if (blueprintCommand?.compose) {
this.generatorsToCompose.push(...blueprintCommand.compose);
}
}
if (blueprintCommand?.override) {
if (this.generatorCommand) {
this.log.warn('Command already set, multiple blueprints may be overriding the command. Unexpected behavior may occur.');
}
// Use the blueprint command if it is set to override.
this.generatorCommand = blueprintCommand;
}
}
return composedBlueprints;
}
/**
* Check if the blueprint implements every priority implemented by the parent generator
* @param {BaseGenerator} blueprintGenerator
*/
#checkBlueprintImplementsPriorities(blueprintGenerator) {
const { taskPrefix: baseGeneratorTaskPrefix = '' } = this.features;
const { taskPrefix: blueprintTaskPrefix = '' } = blueprintGenerator.features;
// v8 remove deprecated priorities
const DEPRECATED_PRIORITIES = new Set(['preConflicts']);
for (const priorityName of Object.values(PRIORITY_NAMES).filter(p => !DEPRECATED_PRIORITIES.has(p))) {
const baseGeneratorPriorityName = `${baseGeneratorTaskPrefix}${priorityName}`;
if (baseGeneratorPriorityName in this) {
const blueprintPriorityName = `${blueprintTaskPrefix}${priorityName}`;
if (!Object.hasOwn(Object.getPrototypeOf(blueprintGenerator), blueprintPriorityName)) {
this.log.debug(`Priority ${blueprintPriorityName} not implemented at ${blueprintGenerator.options.namespace}.`);
}
}
}
}
/**
* @private
* Configure blueprints.
*/
async #configureBlueprints() {
try {
return this.getContextData(CONTEXT_DATA_BLUEPRINTS_TO_COMPOSE);
}
catch {
// Ignore
}
let argvBlueprints = this.options.blueprints || '';
// check for old single blueprint declaration
let { blueprint } = this.options;
if (blueprint) {
if (typeof blueprint === 'string') {
blueprint = [blueprint];
}
this.log.warn('--blueprint option is deprecated. Please use --blueprints instead');
argvBlueprints = union(blueprint, argvBlueprints.split(',')).join(',');
}
const blueprints = mergeBlueprints(parseBlueprints(argvBlueprints), this.jhipsterConfig.blueprints ?? []);
// EnvironmentBuilder already looks for blueprint when running from cli, this is required for tests.
// Can be removed once the tests uses EnvironmentBuilder.
const missingBlueprints = blueprints
.filter(blueprint => !this.env.isPackageRegistered(packageNameToNamespace(blueprint.name)))
.map(blueprint => blueprint.name);
if (missingBlueprints.length > 0) {
await this.env.lookup({ filterPaths: true, packagePatterns: missingBlueprints });
}
if (blueprints && blueprints.length > 0) {
blueprints.forEach(blueprint => {
blueprint.version = this.#findBlueprintVersion(blueprint.name) ?? blueprint.version;
});
this.jhipsterConfig.blueprints = blueprints;
}
if (!this.skipChecks) {
const namespaces = blueprints.map(blueprint => packageNameToNamespace(blueprint.name));
// Verify if the blueprints have been registered.
const missing = namespaces.filter(namespace => !this.env.isPackageRegistered(namespace));
if (missing && missing.length > 0) {
throw new Error(`Some blueprints were not found ${missing}, you should install them manually`);
}
blueprints.forEach(blueprint => {
this.#checkJHipsterBlueprintVersion(blueprint.name);
});
}
const blueprintNames = blueprints.map(blueprint => blueprint.name);
this.getContextData(CONTEXT_DATA_BLUEPRINTS_TO_COMPOSE, {
replacement: blueprintNames,
});
return blueprintNames;
}
/**
* Compose external blueprint module
*/
async #composeBlueprint(blueprint, subGen) {
blueprint = normalizeBlueprintName(blueprint);
if (!this.skipChecks && blueprint !== LOCAL_BLUEPRINT_PACKAGE_NAMESPACE) {
this.#checkBlueprint(blueprint);
}
const generatorName = packageNameToNamespace(blueprint);
const generatorNamespace = `${generatorName}:${subGen}`;
if (!(await this.env.get(generatorNamespace))) {
this.log.debug(`No blueprint found for blueprint ${chalk.yellow(blueprint)} and ${chalk.yellow(subGen)} with namespace ${chalk.yellow(generatorNamespace)} subgenerator: falling back to default generator`);
return undefined;
}
this.log.debug(`Found blueprint ${chalk.yellow(blueprint)} and ${chalk.yellow(subGen)} with namespace ${chalk.yellow(generatorNamespace)}`);
const blueprintGenerator = await this.composeWith(generatorNamespace, {
forwardOptions: true,
schedule: generator => generator.sbsBlueprint,
generatorArgs: this._args,
generatorOptions: {
jhipsterContext: this,
},
});
if (blueprintGenerator instanceof Error) {
throw blueprintGenerator;
}
this._debug(`Using blueprint ${chalk.yellow(blueprint)} for ${chalk.yellow(subGen)} subgenerator`);
return blueprintGenerator;
}
/**
* @private
* Try to retrieve the package.json of the blueprint used, as an object.
* @param {string} blueprintPkgName - generator name
* @return {object} packageJson - retrieved package.json as an object or undefined if not found
*/
#findBlueprintPackageJson(blueprintPkgName) {
const blueprintGeneratorName = packageNameToNamespace(blueprintPkgName);
const blueprintPackagePath = this.env.getPackagePath(blueprintGeneratorName);
if (!blueprintPackagePath) {
this.log.warn(`Could not retrieve packagePath of blueprint '${blueprintPkgName}'`);
return undefined;
}
const packageJsonFile = path.join(blueprintPackagePath, 'package.json');
if (!fs.existsSync(packageJsonFile)) {
return undefined;
}
return JSON.parse(fs.readFileSync(packageJsonFile).toString());
}
/**
* @private
* Try to retrieve the version of the blueprint used.
* @param {string} blueprintPkgName - generator name
* @return {string} version - retrieved version or empty string if not found
*/
#findBlueprintVersion(blueprintPkgName) {
const blueprintPackageJson = this.#findBlueprintPackageJson(blueprintPkgName);
if (!blueprintPackageJson?.version) {
this.log.warn(`Could not retrieve version of blueprint '${blueprintPkgName}'`);
return undefined;
}
return blueprintPackageJson.version;
}
/**
* Check if the generator specified as blueprint is installed.
*/
#checkBlueprint(blueprint) {
if (blueprint === 'generator-jhipster' || blueprint === packageJson.name) {
throw new Error(`You cannot use ${chalk.yellow(blueprint)} as the blueprint.`);
}
}
/**
* Check if the generator specified as blueprint has a version compatible with current JHipster.
*/
#checkJHipsterBlueprintVersion(blueprintPkgName) {
const blueprintPackageJson = this.#findBlueprintPackageJson(blueprintPkgName);
if (!blueprintPackageJson) {
this.log.warn(`Could not retrieve version of JHipster declared by blueprint '${blueprintPkgName}'`);
return;
}
const mainGeneratorJhipsterVersion = packageJson.version;
const compatibleJhipsterRange = blueprintPackageJson.engines?.['generator-jhipster'] ??
blueprintPackageJson.dependencies?.['generator-jhipster'] ??
blueprintPackageJson.peerDependencies?.['generator-jhipster'];
if (compatibleJhipsterRange) {
if (!semver.valid(compatibleJhipsterRange) && !semver.validRange(compatibleJhipsterRange)) {
this.log.verboseInfo(`Blueprint ${blueprintPkgName} contains generator-jhipster dependency with non comparable version`);
return;
}
if (semver.satisfies(mainGeneratorJhipsterVersion, compatibleJhipsterRange, { includePrerelease: true })) {
return;
}
throw new Error(`The installed ${chalk.yellow(blueprintPkgName)} blueprint targets JHipster v${compatibleJhipsterRange} and is not compatible with this JHipster version. Either update the blueprint or JHipster. You can also disable this check using --skip-checks at your own risk`);
}
this.log.warn(`Could not retrieve version of JHipster declared by blueprint '${blueprintPkgName}'`);
}
}
export class CommandBaseGenerator extends BaseGenerator {
}