generator-pyhipster
Version:
Python (Flask) + Angular/React/Vue in one handy generator
531 lines (481 loc) • 18.9 kB
JavaScript
/**
* Copyright 2013-2022 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.
*/
const chalk = require('chalk');
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const pluralize = require('pluralize');
const { fork: forkProcess } = require('child_process');
const EnvironmentBuilder = require('./environment-builder');
const { CLI_NAME, GENERATOR_NAME, logger, toString, printSuccess, getOptionAsArgs } = require('./utils');
const { createImporterFromContent, createImporterFromFiles } = require('../jdl/jdl-importer');
const packagejs = require('../package.json');
const statistics = require('../generators/statistics');
const { JHIPSTER_CONFIG_DIR } = require('../generators/generator-constants');
const jhipsterCli = require.resolve('./cli.js');
const { writeConfigFile } = require('../jdl/exporters/export-utils');
const { createFolderIfItDoesNotExist } = require('../jdl/utils/file-utils');
const getDeploymentType = deployment => deployment && deployment[GENERATOR_NAME] && deployment[GENERATOR_NAME].deploymentType;
/**
* Check if .yo-rc.json exists inside baseName folder.
* @param {string} baseName
* @return {boolean}
*/
const baseNameConfigExists = baseName => fs.existsSync(baseName === undefined ? '.yo-rc.json' : path.join(baseName, '.yo-rc.json'));
const multiplesApplications = processor => {
return Object.values(processor.importState.exportedApplicationsWithEntities).length > 1;
};
/**
* When importing multiples applications, we should import each of them at it's own baseName folder.
* @param {JDLProcessor} processor
* @return {boolean}
*/
const shouldRunInFolder = processor => {
return multiplesApplications(processor);
};
/**
* Check if every application is new.
* @param {JDLProcessor} processor
* @return {boolean}
*/
const allNewApplications = processor => {
const applications = Object.values(processor.importState.exportedApplicationsWithEntities);
if (applications.length === 1) return !baseNameConfigExists();
return !applications.find(application => baseNameConfigExists(application.config.baseName));
};
/**
* Check if the generation should be forced.
* @param {JDLProcessor} processor
* @return {boolean}
*/
const shouldForce = processor => {
if (processor.options.force !== undefined) {
return processor.options.force;
}
if (Object.values(processor.importState.exportedApplicationsWithEntities).length === 0) {
return undefined;
}
return allNewApplications(processor) ? true : undefined;
};
/**
* JHipster will use fork by default only when generating new applications, otherwise it can conflict with incremental changelog.
* @param {JDLProcessor} processor
* @return {boolean}
*/
const shouldFork = processor => {
if (processor.options.fork !== undefined) {
return processor.options.fork;
}
if (Object.values(processor.importState.exportedApplicationsWithEntities).length > 1 && allNewApplications(processor)) {
return true;
}
return undefined;
};
/**
* When regenerating applications we should run interactively, so prompts will be shown by default.
* @param {JDLProcessor} processor
* @return {boolean} true if generation should be executed interactively
*/
const shouldRunInteractively = processor => {
if (processor.options.interactive !== undefined) {
return processor.options.interactive;
}
return !shouldFork(processor);
};
/**
* Write entity config to disk.
* @param {any} entity
* @param {string} basePath
*/
function writeEntityConfig(entity, basePath) {
const entitiesPath = path.join(basePath, JHIPSTER_CONFIG_DIR);
createFolderIfItDoesNotExist(entitiesPath);
const filePath = path.join(entitiesPath, `${_.upperFirst(entity.name)}.json`);
fs.writeFileSync(filePath, JSON.stringify(entity, null, 2).concat('\n'));
}
/**
* Write application config to disk.
* @param {any} applicationWithEntities
* @param {string} basePath
*/
function writeApplicationConfig(applicationWithEntities, basePath) {
createFolderIfItDoesNotExist(basePath);
writeConfigFile({ 'generator-jhipster': applicationWithEntities.config }, path.join(basePath, '.yo-rc.json'));
applicationWithEntities.entities.forEach(entity => writeEntityConfig(entity, basePath));
}
/**
* Run the generator.
* @param {string} command
* @param {Object} options
* @param {string} options.cwd
* @param {boolean} options.fork
* @param {Environment} options.env
* @param {Object} generatorOptions
* @param {Promise}
*/
function runGenerator(command, { cwd, fork, env }, generatorOptions = {}) {
generatorOptions = {
...generatorOptions,
// Remove jdl command exclusive options
fork: undefined,
interactive: undefined,
jsonOnly: undefined,
ignoreApplication: undefined,
ignoreDeployments: undefined,
inline: undefined,
skipSampleRepository: undefined,
workspaces: undefined,
forceNoFiltering: undefined,
unidirectionalRelationships: undefined,
localConfigOnly: undefined,
commandName: undefined,
fromJdl: true,
};
if (!generatorOptions.blueprints) {
delete generatorOptions.blueprints;
}
if (!fork) {
const oldCwd = process.cwd();
process.chdir(cwd);
env = env || EnvironmentBuilder.createDefaultBuilder(undefined, { cwd }).getEnvironment();
return env
.run(`${CLI_NAME}:${command}`, generatorOptions)
.then(
() => {
logger.info(`Generator ${command} succeed`);
},
error => {
logger.error(`Error running generator ${command}: ${error}`, error);
return Promise.reject(error);
}
)
.finally(() => {
process.chdir(oldCwd);
});
}
logger.debug(`Child process will be triggered for ${command} with cwd: ${cwd}`);
const args = [command, ...getOptionAsArgs(generatorOptions)];
const childProc = forkProcess(jhipsterCli, args, {
cwd,
});
return new Promise((resolve, reject) => {
childProc.on('exit', code => {
logger.debug(`Process ${args} exited with code ${code}`);
logger.info(`Generator ${command} child process exited with code ${code}`);
if (code !== 0) {
process.exitCode = code;
reject(new Error(`Error executing ${args.join(' ')}`));
return;
}
resolve();
});
});
}
/**
* Imports the Applications and Entities defined in JDL
* The app .yo-rc.json files and entity json files are written to disk
*/
function importJDL(jdlImporter) {
logger.info('The JDL is being parsed.');
try {
const importState = jdlImporter.import();
logger.debug(`importState exportedEntities: ${importState.exportedEntities.length}`);
logger.debug(`importState exportedApplications: ${importState.exportedApplications.length}`);
logger.debug(`importState exportedDeployments: ${importState.exportedDeployments.length}`);
if (importState.exportedEntities.length > 0) {
const entityNames = _.uniq(importState.exportedEntities.map(exportedEntity => exportedEntity.name)).join(', ');
logger.info(`Found entities: ${chalk.yellow(entityNames)}.`);
} else {
logger.info(chalk.yellow('No change in entity configurations, no entities were updated.'));
}
logger.info('The JDL has been successfully parsed');
return importState;
} catch (error) {
logger.debug('Error:', error);
if (error) {
const errorName = `${error.name}:` || '';
const errorMessage = error.message || '';
logger.log(chalk.red(`${errorName} ${errorMessage}`));
}
logger.error(`Error while parsing applications and entities from the JDL ${error}`, error);
throw error;
}
}
/**
* Check if application needs to be generated
* @param {any} processor
*/
const shouldGenerateApplications = processor =>
!processor.options.ignoreApplication && !processor.options.jsonOnly && processor.importState.exportedApplications.length !== 0;
/**
* Check if deployments needs to be generated
* @param {any} processor
*/
const shouldGenerateDeployments = processor =>
!processor.options.ignoreDeployments && !processor.options.jsonOnly && processor.importState.exportedDeployments.length !== 0;
/**
* Generate deployment source code for JDL deployments defined.
* @param {any} config
* @returns Promise
*/
const generateDeploymentFiles = ({ processor, deployment }) => {
const deploymentType = getDeploymentType(deployment);
logger.info(`Generating deployment ${deploymentType} in a new parallel process`);
logger.debug(`Generating deployment: ${JSON.stringify(deployment[GENERATOR_NAME], null, 2)}`);
const cwd = path.join(processor.pwd, deploymentType);
logger.debug(`Child process will be triggered for ${jhipsterCli} with cwd: ${cwd}`);
return runGenerator(deploymentType, { cwd, fork: false }, { force: true, ...processor.options, skipPrompts: true });
};
/**
* Generate application source code for JDL apps defined.
* @param {any} config
* @returns Promise
*/
const generateApplicationFiles = ({ processor, applicationWithEntities }) => {
logger.debug(`Generating application: ${JSON.stringify(applicationWithEntities.config, null, 2)}`);
const { inFolder, fork, force, reproducible } = processor;
const baseName = applicationWithEntities.config.baseName;
const cwd = inFolder ? path.join(processor.pwd, baseName) : processor.pwd;
if (processor.options.jsonOnly) {
writeApplicationConfig(applicationWithEntities, cwd);
return Promise.resolve();
}
if (fork) {
writeApplicationConfig(applicationWithEntities, cwd);
}
const withEntities = applicationWithEntities.entities.length > 0 ? true : undefined;
const generatorOptions = { reproducible, force, withEntities, ...processor.options };
if (!fork) {
generatorOptions.applicationWithEntities = applicationWithEntities;
}
return runGenerator('app', { cwd, fork }, generatorOptions);
};
/**
* Generate entities for the applications
* @param {any} processor
* @param {any} exportedEntities
* @param {any} env
* @return Promise
*/
const generateEntityFiles = (processor, exportedEntities, env) => {
const { fork, inFolder, force } = processor;
const generatorOptions = {
force,
...processor.options,
};
const callGenerator = baseName => {
const cwd = inFolder && baseName ? path.join(processor.pwd, baseName) : processor.pwd;
if (processor.options.jsonOnly || baseName) {
exportedEntities
.filter(entity => !baseName || entity.applications.includes(baseName))
.forEach(entity => writeEntityConfig(entity, cwd));
if (processor.options.jsonOnly) {
logger.info('Entity JSON files created. Entity generation skipped.');
return Promise.resolve();
}
} else {
generatorOptions.entitiesToImport = exportedEntities;
}
logger.info(`Generating entities for application ${baseName} in a new parallel process`);
logger.debug(`Child process will be triggered for ${jhipsterCli} with cwd: ${cwd}`);
return runGenerator('entities', { cwd, env, fork }, generatorOptions);
};
if (fork) {
/* Generating entities inside multiple apps */
const baseNames = [...new Set(exportedEntities.flatMap(entity => entity.applications))];
if (processor.interactive) {
return baseNames.reduce((promise, baseName) => {
return promise.then(() => callGenerator(baseName));
}, Promise.resolve());
}
return Promise.all(baseNames.map(baseName => callGenerator(baseName)));
}
return callGenerator();
};
class JDLProcessor {
constructor(jdlFiles, jdlContent, options) {
logger.debug(
`JDLProcessor started with ${jdlContent ? `content: ${jdlContent}` : `files: ${jdlFiles}`} and options: ${toString(options)}`
);
this.jdlFiles = jdlFiles;
this.jdlContent = jdlContent;
this.options = options;
this.pwd = process.cwd();
}
importJDL() {
const configuration = {
applicationName: this.options.baseName,
databaseType: this.options.db,
applicationType: this.options.applicationType,
skipUserManagement: this.options.skipUserManagement,
unidirectionalRelationships: this.options.unidirectionalRelationships,
forceNoFiltering: this.options.forceNoFiltering,
generatorVersion: packagejs.version,
skipFileGeneration: true,
};
let importer;
if (this.jdlContent) {
importer = createImporterFromContent(this.jdlContent, configuration);
} else {
importer = createImporterFromFiles(this.jdlFiles, configuration);
}
this.importState = importJDL.call(this, importer);
}
config() {
this.interactive = shouldRunInteractively(this);
this.fork = shouldFork(this);
this.reproducible = allNewApplications(this);
this.inFolder = shouldRunInFolder(this);
this.force = shouldForce(this);
return this;
}
sendInsight() {
statistics.sendSubGenEvent('generator', 'import-jdl');
}
generateWorkspaces(options) {
if (!options.workspaces || !multiplesApplications(this)) {
return Promise.resolve();
}
return EnvironmentBuilder.createDefaultBuilder()
.getEnvironment()
.run('jhipster:workspaces', { workspaces: false, ...options, importState: this.importState });
}
generateApplications() {
if (this.options.ignoreApplication) {
logger.debug('Applications not generated');
return Promise.resolve();
}
const callGenerator = applicationWithEntities => {
try {
return generateApplicationFiles({
processor: this,
applicationWithEntities,
});
} catch (error) {
logger.error(`Error while generating applications from the parsed JDL\n${error}`, error);
throw error;
}
};
const applicationsWithEntities = Object.values(this.importState.exportedApplicationsWithEntities);
logger.info(`Generating ${applicationsWithEntities.length} ${pluralize('application', applicationsWithEntities.length)}.`);
if (applicationsWithEntities.length === 0) {
return Promise.resolve();
}
const allApplications = Object.fromEntries(
applicationsWithEntities.map((applicationWithEntities, applicationIndex) => {
applicationWithEntities.config.applicationIndex = applicationIndex;
return [applicationWithEntities.config.baseName, applicationWithEntities.config];
})
);
applicationsWithEntities.forEach((applicationWithEntities, idx) => {
const relatedApplications = Object.entries(allApplications).filter(
([baseName]) =>
applicationWithEntities.config.baseName !== baseName &&
applicationWithEntities.entities.find(entity => entity.microserviceName === baseName)
);
const { serverPort: gatewayServerPort } = applicationWithEntities.config;
if (relatedApplications.length > 0) {
applicationWithEntities.config.applications = Object.fromEntries(
relatedApplications.map(([baseName, config]) => {
config.gatewayServerPort = gatewayServerPort;
const { clientFramework, serverPort, applicationIndex, devServerPort } = config;
return [baseName, { clientFramework, serverPort, applicationIndex, devServerPort }];
})
);
}
});
if (this.interactive) {
return applicationsWithEntities.reduce((promise, applicationWithEntities) => {
return promise.then(() => callGenerator(applicationWithEntities));
}, Promise.resolve());
}
return Promise.all(applicationsWithEntities.map(applicationWithEntities => callGenerator(applicationWithEntities)));
}
generateDeployments() {
if (!shouldGenerateDeployments(this)) {
logger.debug('Deployments not generated');
return Promise.resolve();
}
logger.info(
`Generating ${this.importState.exportedDeployments.length} ` +
`${pluralize('deployment', this.importState.exportedDeployments.length)}.`
);
const callDeploymentGenerator = () => {
const callGenerator = deployment => {
try {
return generateDeploymentFiles({
processor: this,
deployment,
});
} catch (error) {
logger.error(`Error while generating deployments from the parsed JDL\n${error}`, error);
throw error;
}
};
// Queue callGenerator in chain
return this.importState.exportedDeployments.reduce((promise, deployment) => {
return promise.then(() => callGenerator(deployment));
}, Promise.resolve());
};
return callDeploymentGenerator();
}
generateEntities(env) {
if (this.importState.exportedEntities.length === 0 || shouldGenerateApplications(this)) {
logger.debug('Entities not generated');
return Promise.resolve();
}
try {
logger.info(
`Generating ${this.importState.exportedEntities.length} ${pluralize('entity', this.importState.exportedEntities.length)}.`
);
return generateEntityFiles(this, this.importState.exportedEntities, env);
} catch (error) {
logger.error(`Error while generating entities from the parsed JDL\n${error}`, error);
throw error;
}
}
}
/**
* Import-JDL sub generator
* @param {any} args arguments passed for import-jdl
* @param {any} options options passed from CLI
* @param {any} env the yeoman environment
*/
module.exports = (jdlFiles, options = {}, env) => {
logger.info(chalk.yellow(`Executing import-jdl ${options.inline ? 'with inline content' : jdlFiles.join(' ')}`));
logger.debug(chalk.yellow(`Options: ${toString({ ...options, inline: options.inline ? 'inline content' : '' })}`));
try {
const jdlImporter = new JDLProcessor(jdlFiles, options.inline, options);
jdlImporter.importJDL();
jdlImporter.sendInsight();
jdlImporter.config();
return jdlImporter
.generateWorkspaces(options)
.then(() => jdlImporter.generateApplications())
.then(() => jdlImporter.generateEntities(env))
.then(() => jdlImporter.generateDeployments())
.then(() => {
printSuccess();
return jdlFiles;
});
} catch (e) {
logger.error(`Error during import-jdl: ${e}`, e);
return Promise.reject(new Error(`Error during import-jdl: ${e.message}`));
}
};