UNPKG

generator-pyhipster

Version:

Python (Flask) + Angular/React/Vue in one handy generator

1,469 lines (1,378 loc) 54.5 kB
/** * 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 path = require('path'); const _ = require('lodash'); const Generator = require('yeoman-generator'); const chalk = require('chalk'); const shelljs = require('shelljs'); const semver = require('semver'); const exec = require('child_process').exec; const https = require('https'); const os = require('os'); const { reproducibleConfigForTests: projectNameReproducibleConfigForTests } = require('./project-name/config.cjs'); const packagejs = require('../package.json'); const jhipsterUtils = require('./utils'); const { JAVA_COMPATIBLE_VERSIONS, SERVER_TEST_SRC_DIR, SUPPORTED_CLIENT_FRAMEWORKS, PYTHON_COMPATIBLE_VERSIONS } = require('./generator-constants'); const { languageToJavaLanguage } = require('./utils'); const JSONToJDLEntityConverter = require('../jdl/converters/json-to-jdl-entity-converter'); const JSONToJDLOptionConverter = require('../jdl/converters/json-to-jdl-option-converter'); const { stringify } = require('../utils'); const { ANGULAR, REACT, VUE } = SUPPORTED_CLIENT_FRAMEWORKS; const dbTypes = require('../jdl/jhipster/field-types'); const { REQUIRED } = require('../jdl/jhipster/validations'); const { STRING: TYPE_STRING, INTEGER: TYPE_INTEGER, LONG: TYPE_LONG, BIG_DECIMAL: TYPE_BIG_DECIMAL, FLOAT: TYPE_FLOAT, DOUBLE: TYPE_DOUBLE, UUID: TYPE_UUID, BOOLEAN: TYPE_BOOLEAN, LOCAL_DATE: TYPE_LOCAL_DATE, ZONED_DATE_TIME: TYPE_ZONED_DATE_TIME, INSTANT: TYPE_INSTANT, DURATION: TYPE_DURATION, } = dbTypes.CommonDBTypes; const TYPE_BYTES = dbTypes.RelationalOnlyDBTypes.BYTES; const TYPE_BYTE_BUFFER = dbTypes.RelationalOnlyDBTypes.BYTE_BUFFER; const databaseTypes = require('../jdl/jhipster/database-types'); const { MONGODB, NEO4J, COUCHBASE, CASSANDRA, SQL, ORACLE, MYSQL, POSTGRESQL, MARIADB, MSSQL, H2_DISK, H2_MEMORY } = databaseTypes; const { MAVEN } = require('../jdl/jhipster/build-tool-types'); /** * This is the Generator base private class. * This provides all the private API methods used internally. * These methods should not be directly utilized using commonJS require, * as these can have breaking changes without a major version bump * * The method signatures in private API can be changed without a major version change. */ module.exports = class JHipsterBasePrivateGenerator extends Generator { constructor(args, options, features) { super(args, options, features); // expose lodash to templates this._ = _; } /* ======================================================================== */ /* private methods use within generator (not exposed to modules) */ /* ======================================================================== */ /** * Override yeoman generator's usage function to fine tune --help message. */ usage() { return super.usage().replace('yo jhipster:', 'jhipster '); } /** * Override yeoman generator's destinationPath to apply custom output dir. */ destinationPath(...paths) { paths = path.join(...paths); paths = this.applyOutputPathCustomizer(paths); return paths ? super.destinationPath(paths) : paths; } /** * Install I18N Server Files By Language * * @param {any} _this - reference to generator * @param {string} resourceDir - resource directory * @param {string} lang - language code */ installI18nServerFilesByLanguage(_this, resourceDir, lang, testResourceDir) { const generator = _this || this; const prefix = this.fetchFromInstalledJHipster('languages/templates'); const langJavaProp = languageToJavaLanguage(lang); generator.template( `${prefix}/${resourceDir}i18n/messages_${langJavaProp}.properties.ejs`, `${resourceDir}i18n/messages_${langJavaProp}.properties` ); if (!this.skipUserManagement) { generator.template( `${prefix}/${testResourceDir}i18n/messages_${langJavaProp}.properties.ejs`, `${testResourceDir}i18n/messages_${langJavaProp}.properties` ); } } /** * Update Languages In Language Constant * * @param languages */ updateLanguagesInLanguageConstant(languages) { const fullPath = `${this.CLIENT_MAIN_SRC_DIR}app/components/language/language.constants.js`; try { let content = ".constant('LANGUAGES', [\n"; languages.forEach((language, i) => { content += ` '${language}'${i !== languages.length - 1 ? ',' : ''}\n`; }); content += ' // jhipster-needle-i18n-language-constant - JHipster will add/remove languages in this array\n ]'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /\.constant.*LANGUAGES.*\[([^\]]*jhipster-needle-i18n-language-constant[^\]]*)\]/g, content, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. LANGUAGE constant not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } /** * Update Languages In Language Constant NG2 * * @param languages */ updateLanguagesInLanguageConstantNG2(languages) { if (this.clientFramework !== ANGULAR) { return; } const fullPath = `${this.CLIENT_MAIN_SRC_DIR}app/config/language.constants.ts`; try { let content = 'export const LANGUAGES: string[] = [\n'; languages.forEach((language, i) => { content += ` '${language}'${i !== languages.length - 1 ? ',' : ''}\n`; }); content += ' // jhipster-needle-i18n-language-constant - JHipster will add/remove languages in this array\n];'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /export.*LANGUAGES.*\[([^\]]*jhipster-needle-i18n-language-constant[^\]]*)\];/g, content, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. LANGUAGE constant not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } /** * Update Languages In MailServiceIT * * @param languages */ updateLanguagesInLanguageMailServiceIT(languages, packageFolder) { const fullPath = `${SERVER_TEST_SRC_DIR}${packageFolder}/service/MailServiceIT.java`; try { let content = 'private static final String[] languages = {\n'; languages.forEach((language, i) => { content += ` "${language}"${i !== languages.length - 1 ? ',' : ''}\n`; }); content += ' // jhipster-needle-i18n-language-constant - JHipster will add/remove languages in this array\n };'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /private.*static.*String.*languages.*\{([^}]*jhipster-needle-i18n-language-constant[^}]*)\};/g, content, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. LANGUAGE constant not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } /** * Update Languages In Language Pipe * * @param languages */ updateLanguagesInLanguagePipe(languages) { const fullPath = this.clientFramework === ANGULAR ? `${this.CLIENT_MAIN_SRC_DIR}app/shared/language/find-language-from-key.pipe.ts` : `${this.CLIENT_MAIN_SRC_DIR}/app/config/translation.ts`; try { let content = '{\n'; this.generateLanguageOptions(languages, this.clientFramework).forEach((ln, i) => { content += ` ${ln}${i !== languages.length - 1 ? ',' : ''}\n`; }); content += ' // jhipster-needle-i18n-language-key-pipe - JHipster will add/remove languages in this object\n };'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /{\s*('[a-z-]*':)?([^=]*jhipster-needle-i18n-language-key-pipe[^;]*)\};/g, content, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. Language pipe not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } /** * Update Languages In Webpack * * @param languages */ updateLanguagesInWebpackAngular(languages) { const fullPath = 'webpack/webpack.custom.js'; try { let content = 'groupBy: [\n'; // prettier-ignore languages.forEach((language, i) => { content += ` { pattern: "./${this.CLIENT_MAIN_SRC_DIR}i18n/${language}/*.json", fileName: "./i18n/${language}.json" }${ i !== languages.length - 1 ? ',' : '' }\n`; }); content += ' // jhipster-needle-i18n-language-webpack - JHipster will add/remove languages in this array\n' + ' ]'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /groupBy:.*\[([^\]]*jhipster-needle-i18n-language-webpack[^\]]*)\]/g, content, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. Webpack language task not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } /** * Update Languages In Webpack React * * @param languages */ updateLanguagesInWebpackReact(languages) { const fullPath = 'webpack/webpack.common.js'; try { let content = 'groupBy: [\n'; // prettier-ignore languages.forEach((language, i) => { content += ` { pattern: "./${this.CLIENT_MAIN_SRC_DIR}i18n/${language}/*.json", fileName: "./i18n/${language}.json" }${ i !== languages.length - 1 ? ',' : '' }\n`; }); content += ' // jhipster-needle-i18n-language-webpack - JHipster will add/remove languages in this array\n' + ' ]'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /groupBy:.*\[([^\]]*jhipster-needle-i18n-language-webpack[^\]]*)\]/g, content, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. Webpack language task not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } /** * Update DayJS Locales to keep in dayjs.ts config file * * @param languages */ updateLanguagesInDayjsConfiguation(languages) { let fullPath = `${this.CLIENT_MAIN_SRC_DIR}app/config/dayjs.ts`; if (this.clientFramework === VUE) { fullPath = `${this.CLIENT_MAIN_SRC_DIR}app/shared/config/dayjs.ts`; } else if (this.clientFramework === ANGULAR) { fullPath = `${this.CLIENT_MAIN_SRC_DIR}app/config/dayjs.ts`; } try { const content = languages.reduce( (content, language) => `${content}import 'dayjs/${this.clientFrameworkAngular ? 'esm/' : ''}locale/${this.getDayjsLocaleId(language)}'\n`, '// jhipster-needle-i18n-language-dayjs-imports - JHipster will import languages from dayjs here\n' ); jhipsterUtils.replaceContent( { file: fullPath, // match needle until // DAYJS CONFIGURATION (excluded) pattern: /\/\/ jhipster-needle-i18n-language-dayjs-imports[\s\S]+?(?=\/\/ DAYJS CONFIGURATION)/g, content: `${content}\n`, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. DayJS language task not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } /** * Remove File * * @param file */ removeFile(file) { file = this.destinationPath(file); if (file && shelljs.test('-f', file)) { this.log(`Removing the file - ${file}`); shelljs.rm(file); } } /** * Remove Folder * * @param folder */ removeFolder(folder) { folder = this.destinationPath(folder); if (folder && shelljs.test('-d', folder)) { this.log(`Removing the folder - ${folder}`); shelljs.rm('-rf', folder); } } /** * Execute a git mv. * * @param {string} source * @param {string} dest * @returns {boolean} true if success; false otherwise */ gitMove(source, dest) { source = this.destinationPath(source); dest = this.destinationPath(dest); if (source && dest && shelljs.test('-f', source)) { this.info(`Renaming the file - ${source} to ${dest}`); return !shelljs.exec(`git mv -f ${source} ${dest}`).code; } return true; } poetryInstall() { this.info('Installing Python modules') return !shelljs.exec(`poetry install`).code; } /** * @returns default app name */ getDefaultAppName() { if (this.options.reproducible) { return projectNameReproducibleConfigForTests.baseName; } return /^[a-zA-Z0-9_]+$/.test(path.basename(process.cwd())) ? path.basename(process.cwd()) : 'pyhipster'; } /** * Format As Class Javadoc * * @param {string} text - text to format * @returns class javadoc */ formatAsClassJavadoc(text) { return jhipsterUtils.getJavadoc(text, 0); } /** * Format As Field Javadoc * * @param {string} text - text to format * @returns field javadoc */ formatAsFieldJavadoc(text) { return jhipsterUtils.getJavadoc(text, 4); } /** * Format As Api Description * * @param {string} text - text to format * @returns formatted api description */ formatAsApiDescription(text) { if (!text) { return text; } const rows = text.split('\n'); let description = this.formatLineForJavaStringUse(rows[0]); for (let i = 1; i < rows.length; i++) { // discard empty rows if (rows[i].trim() !== '') { // if simple text then put space between row strings if (!description.endsWith('>') && !rows[i].startsWith('<')) { description += ' '; } description += this.formatLineForJavaStringUse(rows[i]); } } return description; } formatLineForJavaStringUse(text) { if (!text) { return text; } return text.replace(/"/g, '\\"'); } /** * Format As Liquibase Remarks * * @param {string} text - text to format * @param {boolean} addRemarksTag - add remarks tag * @returns formatted liquibase remarks */ formatAsLiquibaseRemarks(text, addRemarksTag = false) { if (!text) { return addRemarksTag ? '' : text; } const rows = text.split('\n'); let description = rows[0]; for (let i = 1; i < rows.length; i++) { // discard empty rows if (rows[i].trim() !== '') { // if simple text then put space between row strings if (!description.endsWith('>') && !rows[i].startsWith('<')) { description += ' '; } description += rows[i]; } } // escape & to &amp; description = description.replace(/&/g, '&amp;'); // escape " to &quot; description = description.replace(/"/g, '&quot;'); // escape ' to &apos; description = description.replace(/'/g, '&apos;'); // escape < to &lt; description = description.replace(/</g, '&lt;'); // escape > to &gt; description = description.replace(/>/g, '&gt;'); return addRemarksTag ? ` remarks="${description}"` : description; } /** * Parse creationTimestamp option * @returns {number} representing the milliseconds elapsed since January 1, 1970, 00:00:00 UTC * obtained by parsing the given string representation of the creationTimestamp. */ parseCreationTimestamp(creationTimestampOption = this.options.creationTimestamp) { let creationTimestamp; if (creationTimestampOption) { creationTimestamp = Date.parse(creationTimestampOption); if (!creationTimestamp) { this.warning(`Error parsing creationTimestamp ${creationTimestampOption}.`); } else if (creationTimestamp > new Date().getTime()) { this.error(`Creation timestamp should not be in the future: ${creationTimestampOption}.`); } } return creationTimestamp; } /** * @param {any} input input * @returns {boolean} true if input is number; false otherwise */ isNumber(input) { if (isNaN(this.filterNumber(input))) { return false; } return true; } /** * @param {any} input input * @returns {boolean} true if input is a signed number; false otherwise */ isSignedNumber(input) { if (isNaN(this.filterNumber(input, true))) { return false; } return true; } /** * @param {any} input input * @returns {boolean} true if input is a signed decimal number; false otherwise */ isSignedDecimalNumber(input) { if (isNaN(this.filterNumber(input, true, true))) { return false; } return true; } /** * Filter Number * * @param {string} input - input to filter * @param isSigned - flag indicating whether to check for signed number or not * @param isDecimal - flag indicating whether to check for decimal number or not * @returns {number} parsed number if valid input; <code>NaN</code> otherwise */ filterNumber(input, isSigned, isDecimal) { const signed = isSigned ? '(\\-|\\+)?' : ''; const decimal = isDecimal ? '(\\.[0-9]+)?' : ''; const regex = new RegExp(`^${signed}([0-9]+${decimal})$`); if (regex.test(input)) return Number(input); return NaN; } /** * Checks if git is installed. * * @param {function} callback[optional] - function to be called after checking if git is installed. The callback will receive the code of the shell command executed. * * @return {boolean} true if installed; false otherwise. */ isGitInstalled(callback) { const gitInstalled = jhipsterUtils.isGitInstalled(callback); if (!gitInstalled) { this.warning('git is not found on your computer.\n', ` Install git: ${chalk.yellow('https://git-scm.com/')}`); } else { exec('git --version', (err, stdout, stderr) => { if (err) { this.warning('Git is not found on your computer.'); } else { const gitVersion = stdout.match(/(?:git version) (\d.\d{2}.\d)/)[1]; this.info(`Your Git version is: ${chalk.yellow(gitVersion)}`); } }); } return gitInstalled; } /** * Initialize git repository. */ initializeGitRepository() { if (this.gitInstalled || this.isGitInstalled()) { const gitDir = this.gitExec('rev-parse --is-inside-work-tree', { trace: false }).stdout; // gitDir has a line break to remove (at least on windows) if (gitDir && gitDir.trim() === 'true') { this.gitInitialized = true; } else { const shellStr = this.gitExec('init', { trace: false }); this.gitInitialized = shellStr.code === 0; if (this.gitInitialized) this.log(chalk.green.bold('Git repository initialized.')); else this.warning(`Failed to initialize Git repository.\n ${shellStr.stderr}`); } } else { this.warning('Git repository could not be initialized, as Git is not installed on your system'); } } /** * Commit pending files to git. */ commitFilesToGit(commitMsg, done) { if (this.gitInitialized) { this.debug('Committing files to git'); this.gitExec('log --oneline -n 1 -- .', { trace: false }, (code, commits) => { if (code !== 0 || !commits || !commits.trim()) { // if no files in Git from current folder then we assume that this is initial application generation this.gitExec('add .', { trace: false }, code => { if (code === 0) { this.gitExec(`commit --no-verify -m "${commitMsg}" -- .`, { trace: false }, code => { if (code === 0) { this.log(chalk.green.bold(`Application successfully committed to Git from ${process.cwd()}.`)); } else { this.log(chalk.red.bold(`Application commit to Git failed from ${process.cwd()}. Try to commit manually.`)); } done(); }); } else { this.warning(`The generated application could not be committed to Git, because ${chalk.bold('git add')} command failed.`); done(); } }); } else { // if found files in Git from current folder then we assume that this is application regeneration // if there are changes in current folder then inform user about manual commit needed this.gitExec('diff --name-only .', { trace: false }, (code, diffs) => { if (code === 0 && diffs && diffs.trim()) { this.log( `Found commits in Git from ${process.cwd()}. So we assume this is application regeneration. Therefore automatic Git commit is not done. You can do Git commit manually.` ); } done(); }); } }); } else { this.warning('The generated application could not be committed to Git, as a Git repository could not be initialized.'); done(); } } /** * Get Option From Array * * @param {Array} array - array * @param {any} option - options * @returns {boolean} true if option is in array and is set to 'true' */ getOptionFromArray(array, option) { let optionValue = false; array.forEach(value => { if (_.includes(value, option)) { optionValue = value.split(':')[1]; } }); optionValue = optionValue === 'true' ? true : optionValue; return optionValue; } /** * Function to issue a https get request, and process the result * * @param {string} url - the url to fetch * @param {function} onSuccess - function, which gets called when the request succeeds, with the body of the response * @param {function} onFail - callback when the get failed. */ httpsGet(url, onSuccess, onFail) { https .get(url, res => { let body = ''; res.on('data', chunk => { body += chunk; }); res.on('end', () => { onSuccess(body); }); }) .on('error', onFail); } /** * Strip margin indicated by pipe `|` from a string literal * * @param {string} content - the string to process */ stripMargin(content) { return content.replace(/^[ ]*\|/gm, ''); } /** * Utility function to copy and process templates. * * @param {string} source - source * @param {string} destination - destination * @param {*} generator - reference to the generator * @param {*} options - options object * @param {*} context - context */ template(source, destination, generator, options = {}, context) { const _this = generator || this; const _context = context || _this; const customDestination = _this.destinationPath(destination); if (!customDestination) { this.debug(`File ${destination} ignored`); return Promise.resolved(); } return jhipsterUtils .renderContent(source, _this, _context, options) .then(res => { _this.fs.write(customDestination, res); return customDestination; }) .catch(error => { this.warning(source); throw error; }); } /** * Utility function to render a template into a string * * @param {string} source - source * @param {function} callback - callback to take the rendered template as a string * @param {*} generator - reference to the generator * @param {*} options - options object * @param {*} context - context */ render(source, callback, generator, options = {}, context) { const _this = generator || this; const _context = context || _this; jhipsterUtils.renderContent(source, _this, _context, options, res => { callback(res); }); } /** * Utility function to copy files. * * @param {string} source - Original file. * @param {string} destination - The resulting file. */ copy(source, destination) { const customDestination = this.destinationPath(destination); if (!customDestination) { this.debug(`File ${destination} ignored`); return; } this.fs.copy(this.templatePath(source), customDestination); } /** * Print a debug message. * * @param {string} msg - message to print * @param {string[]} args - arguments to print */ debug(msg, ...args) { const formattedMsg = `${chalk.yellow.bold('DEBUG!')} ${msg}`; if ((this.configOptions && this.configOptions.isDebugEnabled) || (this.options && this.options.debug)) { this.log(formattedMsg); args.forEach(arg => this.log(arg)); } if (this._debug && this._debug.enabled) { this._debug(formattedMsg); args.forEach(arg => this._debug(arg)); } } /** * Check if Java is installed */ checkJava() { if (this.skipChecks || this.skipServer) return; const done = this.async(); exec('java -version', (err, stdout, stderr) => { if (err) { this.warning('Java is not found on your computer.'); } else { const javaVersion = stderr.match(/(?:java|openjdk) version "(.*)"/)[1]; if (!javaVersion.match(new RegExp(`(${JAVA_COMPATIBLE_VERSIONS.map(ver => `^${ver}`).join('|')})`))) { const [latest, ...others] = JAVA_COMPATIBLE_VERSIONS.concat().reverse(); this.warning( `Java ${others.reverse().join(', ')} or ${latest} are not found on your computer. Your Java version is: ${chalk.yellow( javaVersion )}` ); } else { this.info(`Your Java version is: ${chalk.yellow(javaVersion)}`) } } done(); }); } /** * Check if Python is installed */ checkPython() { if (this.skipChecks || this.skipServer) return; const done = this.async(); var python = ''; if (os.platform() === 'win32') { python = 'python --version' } else { python = 'python3 --version' } exec(python, (err, stdout, stderr) => { if (err) { this.warning('Python is not found on your computer.'); } else { const pythonVersion = stdout.match(/(?:Python) (\d.\d.?)/)[1]; if (!pythonVersion.match(new RegExp(`(${PYTHON_COMPATIBLE_VERSIONS.map(ver => `^${ver}`).join('|')})`))) { const [latest, ...others] = PYTHON_COMPATIBLE_VERSIONS.concat().reverse(); this.warning( `Python ${others.reverse().join(', ')} or ${latest} are not found on your computer. Your Python version is: ${chalk.yellow( pythonVersion )}` ); } else { this.info(`Your Python version is: ${chalk.yellow(pythonVersion)}`) } } done(); }); } /** * Check if Node is installed */ checkNode() { if (this.skipChecks) return; const nodeFromPackageJson = packagejs.engines.node; if (!semver.satisfies(process.version, nodeFromPackageJson)) { this.warning( `Your NodeJS version is too old (${process.version}). You should use at least NodeJS ${chalk.bold(nodeFromPackageJson)}` ); } if (!(process.release || {}).lts) { this.warning( 'Your Node version is not LTS (Long Term Support), use it at your own risk! JHipster does not support non-LTS releases, so if you encounter a bug, please use a LTS version first.' ); } else { this.info(`Your Node version is: ${chalk.yellow(process.version)}`) } } /** * Check if Git is installed */ checkGit() { if (this.skipChecks || this.skipClient) return; this.gitInstalled = this.isGitInstalled(); } /** * Generate Entity Client Field Default Values * * @param {Array|Object} fields - array of fields * @returns {Array} defaultVariablesValues */ generateEntityClientFieldDefaultValues(fields, clientFramework = ANGULAR) { const defaultVariablesValues = {}; fields.forEach(field => { const fieldType = field.fieldType; const fieldName = field.fieldName; if (fieldType === TYPE_BOOLEAN) { if (clientFramework === REACT) { defaultVariablesValues[fieldName] = `${fieldName}: false,`; } else { defaultVariablesValues[fieldName] = `this.${fieldName} = this.${fieldName} ?? false;`; } } }); return defaultVariablesValues; } /** * Find key type for Typescript * * @param {string} primaryKey - primary key definition * @returns {string} primary key type in Typescript */ getTypescriptKeyType(primaryKey) { if (typeof primaryKey === 'object') { primaryKey = primaryKey.type; } if ([TYPE_INTEGER, TYPE_LONG, TYPE_FLOAT, TYPE_DOUBLE, TYPE_BIG_DECIMAL].includes(primaryKey)) { return 'number'; } return 'string'; } /** * Generate Entity Client Field Declarations * * @param {string} primaryKey - primary key definition * @param {Array|Object} fields - array of fields * @param {Array|Object} relationships - array of relationships * @param {string} dto - dto * @param {boolean} embedded - either the actual entity is embedded or not * @returns variablesWithTypes: Array */ generateEntityClientFields(primaryKey, fields, relationships, dto, customDateType = 'dayjs.Dayjs', embedded = false) { const variablesWithTypes = []; if (!embedded && primaryKey) { const tsKeyType = this.getTypescriptKeyType(primaryKey); if (this.jhipsterConfig.clientFramework === VUE) { variablesWithTypes.push(`id?: ${tsKeyType}`); } } fields.forEach(field => { const fieldType = field.fieldType; const fieldName = field.fieldName; const nullable = !field.id && field.nullable; let tsType = 'any'; if (field.fieldIsEnum) { tsType = fieldType; } else if (fieldType === TYPE_BOOLEAN) { tsType = 'boolean'; } else if ([TYPE_INTEGER, TYPE_LONG, TYPE_FLOAT, TYPE_DOUBLE, TYPE_BIG_DECIMAL].includes(fieldType)) { tsType = 'number'; } else if ([TYPE_STRING, TYPE_UUID, TYPE_DURATION, TYPE_BYTES, TYPE_BYTE_BUFFER].includes(fieldType)) { tsType = 'string'; if ([TYPE_BYTES, TYPE_BYTE_BUFFER].includes(fieldType) && field.fieldTypeBlobContent !== 'text') { variablesWithTypes.push(`${fieldName}ContentType?: ${nullable ? 'string | null' : 'string'}`); } } else if ([TYPE_LOCAL_DATE, TYPE_INSTANT, TYPE_ZONED_DATE_TIME].includes(fieldType)) { tsType = customDateType; } if (nullable) { tsType += ' | null'; } variablesWithTypes.push(`${fieldName}?: ${tsType}`); }); relationships.forEach(relationship => { let fieldType; let fieldName; const nullable = !relationship.relationshipValidateRules || !relationship.relationshipValidateRules.includes(REQUIRED); const relationshipType = relationship.relationshipType; if (relationshipType === 'one-to-many' || relationshipType === 'many-to-many') { fieldType = `I${relationship.otherEntityAngularName}[]`; fieldName = relationship.relationshipFieldNamePlural; } else { fieldType = `I${relationship.otherEntityAngularName}`; fieldName = relationship.relationshipFieldName; } if (nullable) { fieldType += ' | null'; } variablesWithTypes.push(`${fieldName}?: ${fieldType}`); }); return variablesWithTypes; } /** * Generate Entity Client Imports * * @param {Array|Object} relationships - array of relationships * @param {string} dto - dto * @param {string} clientFramework the client framework, 'angularX' or 'react'. * @returns typeImports: Map */ generateEntityClientImports(relationships, dto, clientFramework = this.clientFramework) { const typeImports = new Map(); relationships.forEach(relationship => { const otherEntityAngularName = relationship.otherEntityAngularName; const importType = `I${otherEntityAngularName}`; let importPath; if (this.isBuiltInUser(otherEntityAngularName)) { importPath = clientFramework === ANGULAR ? 'app/entities/user/user.model' : 'app/shared/model/user.model'; } else { importPath = clientFramework === ANGULAR ? `app/entities/${relationship.otherEntityClientRootFolder}${relationship.otherEntityFolderName}/${relationship.otherEntityFileName}.model` : `app/shared/model/${relationship.otherEntityClientRootFolder}${relationship.otherEntityFileName}.model`; } typeImports.set(importType, importPath); }); return typeImports; } /** * Generate Entity Client Enum Imports * * @param {Array|Object} fields - array of the entity fields * @param {string} clientFramework the client framework, 'angularX' or 'react'. * @returns typeImports: Map */ generateEntityClientEnumImports(fields, clientFramework = this.clientFramework) { const typeImports = new Map(); const uniqueEnums = {}; fields.forEach(field => { const fileName = _.kebabCase(field.fieldType); if (field.fieldIsEnum && (!uniqueEnums[field.fieldType] || (uniqueEnums[field.fieldType] && field.fieldValues.length !== 0))) { const importType = `${field.fieldType}`; const basePath = clientFramework === VUE ? '@' : 'app'; const modelPath = clientFramework === ANGULAR ? 'entities' : 'shared/model'; const importPath = `${basePath}/${modelPath}/enumerations/${fileName}.model`; uniqueEnums[field.fieldType] = field.fieldType; typeImports.set(importType, importPath); } }); return typeImports; } /** * Get DB type from DB value * @param {string} db - db */ getDBTypeFromDBValue(db) { return jhipsterUtils.getDBTypeFromDBValue(db); } /** * Get build directory used by buildTool * @param {string} buildTool - buildTool */ getBuildDirectoryForBuildTool(buildTool) { return buildTool === MAVEN ? 'target/' : 'build/'; } /** * Get resource build directory used by buildTool * @param {string} buildTool - buildTool */ getResourceBuildDirectoryForBuildTool(buildTool) { return buildTool === MAVEN ? 'target/classes/' : 'build/resources/main/'; } /** * @returns generated JDL from entities */ generateJDLFromEntities() { let jdlObject; const entities = new Map(); try { this.getExistingEntities().forEach(entity => { entities.set(entity.name, entity.definition); }); jdlObject = JSONToJDLEntityConverter.convertEntitiesToJDL({ entities }); JSONToJDLOptionConverter.convertServerOptionsToJDL({ 'generator-jhipster': this.config.getAll() }, jdlObject); } catch (error) { this.log(error.message || error); this.error('\nError while parsing entities to JDL\n'); } return jdlObject; } /** * Generate language objects in array of "'en': { name: 'English' }" format * @param {string[]} languages * @returns generated language options */ generateLanguageOptions(languages, clientFramework) { const selectedLangs = this.getAllSupportedLanguageOptions().filter(lang => languages.includes(lang.value)); if (clientFramework === REACT) { return selectedLangs.map(lang => `'${lang.value}': { name: '${lang.dispName}'${lang.rtl ? ', rtl: true' : ''} }`); } return selectedLangs.map(lang => `'${lang.value}': { name: '${lang.dispName}'${lang.rtl ? ', rtl: true' : ''} }`); } /** * Check if language should be skipped for locale setting * @param {string} language */ skipLanguageForLocale(language) { const out = this.getAllSupportedLanguageOptions().filter(lang => language === lang.value); return out && out[0] && !!out[0].skipForLocale; } /** * Return the method name which converts the filter to specification * @param {string} fieldType */ getSpecificationBuilder(fieldType) { if ( [ TYPE_INTEGER, TYPE_LONG, TYPE_FLOAT, TYPE_DOUBLE, TYPE_BIG_DECIMAL, TYPE_LOCAL_DATE, TYPE_ZONED_DATE_TIME, TYPE_INSTANT, TYPE_DURATION, ].includes(fieldType) ) { return 'buildRangeSpecification'; } if (fieldType === TYPE_STRING) { return 'buildStringSpecification'; } return 'buildSpecification'; } /** * @param {string} fieldType * @returns {boolean} true if type is filterable; false otherwise. */ isFilterableType(fieldType) { return ![TYPE_BYTES, TYPE_BYTE_BUFFER].includes(fieldType); } /** * Rebuild client for Angular */ rebuildClient() { const done = this.async(); this.log(`\n${chalk.bold.green('Running `webapp:build` to update client app\n')}`); this.spawnCommand(this.clientPackageManager, ['run', 'webapp:build']).on('close', () => { done(); }); } /** * Generate a primary key, according to the type * * @param {any} primaryKey - primary key definition * @param {number} index - the index of the primary key, currently it's possible to generate 2 values, index = 0 - first key (default), otherwise second key * @param {boolean} [wrapped=true] - wrapped values for required types. */ generateTestEntityId(primaryKey, index = 0, wrapped = true) { if (typeof primaryKey === 'object') { primaryKey = primaryKey.type; } let value; if (primaryKey === TYPE_STRING) { value = index === 0 ? 'ABC' : 'CBA'; } else if (primaryKey === TYPE_UUID) { value = index === 0 ? '9fec3727-3421-4967-b213-ba36557ca194' : '1361f429-3817-4123-8ee3-fdf8943310b2'; } else { value = index === 0 ? 123 : 456; } if (wrapped && [TYPE_UUID, TYPE_STRING].includes(primaryKey)) { return `'${value}'`; } return value; } /** * Generate a test entity, according to the type * * @param {any} primaryKey - primary key definition. * @param {number} [index] - index of the primary key sample, pass undefined for a random key. */ generateTestEntityPrimaryKey(primaryKey, index) { return JSON.stringify( this.generateTestEntity( primaryKey.fields.map(f => f.reference), index ) ); } /** * Generate a test entity, according to the type * * @param {any} primaryKey - primary key definition. * @param {number} [index] - index of the primary key sample, pass undefined for a random key. */ generateTestEntity(references, index = 'random') { const random = index === 'random'; const entries = references .map(reference => { if (random && reference.field) { const field = reference.field; const fakeData = field.generateFakeData('json-serializable'); if (reference.field.fieldWithContentType) { return [ [reference.name, fakeData], [field.contentTypeFieldName, 'unknown'], ]; } return [[reference.name, fakeData]]; } return [[reference.name, this.generateTestEntityId(reference.type, index, false)]]; }) .flat(); return Object.fromEntries(entries); } /** * Return the primary key data type based on DB * * @param {any} databaseType - the database type */ getPkType(databaseType) { if (this.jhipsterConfig.pkType) { return this.jhipsterConfig.pkType; } if ([MONGODB, NEO4J, COUCHBASE].includes(databaseType)) { return TYPE_STRING; } if (databaseType === CASSANDRA) { return TYPE_UUID; } return TYPE_LONG; } /** * Returns the URL for a particular databaseType and protocol * * @param {string} databaseType * @param {string} protocol * @param {*} options */ getDBCUrl(databaseType, protocol, options = {}) { if (!protocol) { throw new Error('protocol is required'); } if (!options.databaseName) { throw new Error("option 'databaseName' is required"); } if ([MYSQL, MARIADB, POSTGRESQL, ORACLE, MSSQL].includes(databaseType) && !options.hostname) { throw new Error(`option 'hostname' is required for ${databaseType} databaseType`); } let dbcUrl; let extraOptions; if (databaseType === MYSQL) { dbcUrl = `${protocol}:mysql://${options.hostname}:3306/${options.databaseName}`; extraOptions = '?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC&createDatabaseIfNotExist=true'; } else if (databaseType === MARIADB) { dbcUrl = `${protocol}:mariadb://${options.hostname}:3306/${options.databaseName}`; extraOptions = '?useLegacyDatetimeCode=false&serverTimezone=UTC'; } else if (databaseType === POSTGRESQL) { dbcUrl = `${protocol}:postgresql://${options.hostname}:5432/${options.databaseName}`; } else if (databaseType === ORACLE) { dbcUrl = `${protocol}:oracle:thin:@${options.hostname}:1521:${options.databaseName}`; } else if (databaseType === MSSQL) { if (protocol === 'r2dbc') { dbcUrl = `${protocol}:mssql://${options.hostname}:1433/${options.databaseName}`; } else { dbcUrl = `${protocol}:sqlserver://${options.hostname}:1433;database=${options.databaseName}`; } } else if (databaseType === H2_DISK) { if (!options.localDirectory) { throw new Error(`'localDirectory' option should be provided for ${databaseType} databaseType`); } if (protocol === 'r2dbc') { dbcUrl = `${protocol}:h2:file://${options.localDirectory}/${options.databaseName}`; } else { dbcUrl = `${protocol}:h2:file:${options.localDirectory}/${options.databaseName}`; } extraOptions = ';DB_CLOSE_DELAY=-1'; } else if (databaseType === H2_MEMORY) { if (protocol === 'r2dbc') { dbcUrl = `${protocol}:h2:mem:///${options.databaseName}`; } else { dbcUrl = `${protocol}:h2:mem:${options.databaseName}`; } extraOptions = ';DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE'; } else { throw new Error(`${databaseType} databaseType is not supported`); } if (!options.skipExtraOptions && extraOptions) { dbcUrl += extraOptions; } return dbcUrl; } /** * Returns the primary key value based on the primary key type, DB and default value * * @param {string} primaryKeyType - the primary key type * @param {string} databaseType - the database type * @param {string} defaultValue - default value * @returns {string} java primary key value */ getPrimaryKeyValue(primaryKey, databaseType = this.jhipsterConfig.databaseType, defaultValue = 1) { if (typeof primaryKey === 'object' && primaryKey.composite) { return `new ${primaryKey.type}(${primaryKey.references .map(ref => this.getPrimaryKeyValue(ref.type, databaseType, defaultValue)) .join(', ')})`; } const primaryKeyType = typeof primaryKey === 'string' ? primaryKey : primaryKey.type; if (primaryKeyType === TYPE_STRING) { if (databaseType === SQL && defaultValue === 0) { return this.getJavaValueGeneratorForType(primaryKeyType); } return `"id${defaultValue}"`; } if (primaryKeyType === TYPE_UUID) { return this.getJavaValueGeneratorForType(primaryKeyType); } return `${defaultValue}L`; } getJavaValueGeneratorForType(type) { if (type === 'String') { return 'UUID.randomUUID().toString()'; } if (type === 'UUID') { return 'UUID.randomUUID()'; } if (type === 'Long') { return 'count.incrementAndGet()'; } throw new Error(`Java type ${type} does not have a random generator implemented`); } /** * Get a root folder name for entity * @param {string} clientRootFolder * @param {string} entityFileName */ getEntityFolderName(clientRootFolder, entityFileName) { if (clientRootFolder) { return `${clientRootFolder}/${entityFileName}`; } return entityFileName; } /** * Get a parent folder path addition for entity * @param {string} clientRootFolder */ getEntityParentPathAddition(clientRootFolder) { if (!clientRootFolder) { return ''; } const relative = path.relative(`/app/entities/${clientRootFolder}/`, '/app/entities/'); if (relative.includes('app')) { // Relative path outside angular base dir. const message = ` "clientRootFolder outside app base dir '${clientRootFolder}'" `; // Test case doesn't have a environment instance so return 'error' if (this.env === undefined) { throw new Error(message); } this.error(message); } const entityFolderPathAddition = relative.replace(/[/|\\]?..[/|\\]entities/, '').replace('entities', '..'); if (!entityFolderPathAddition) { return ''; } return `${entityFolderPathAddition}/`; } /** * Check if the subgenerator has been invoked from JHipster CLI or from Yeoman (yo jhipster:subgenerator) */ checkInvocationFromCLI() { if (!this.options.fromCli) { this.warning( `Deprecated: JHipster seems to be invoked using Yeoman command. Please use the JHipster CLI. Run ${chalk.red( 'jhipster <command>' )} instead of ${chalk.red('yo jhipster:<command>')}` ); } } vueUpdateLanguagesInTranslationStore(languages) { const fullPath = `${this.CLIENT_MAIN_SRC_DIR}app/shared/config/store/translation-store.ts`; try { let content = 'languages: {\n'; if (this.enableTranslation) { this.generateLanguageOptions(languages, this.clientFramework).forEach((ln, i) => { content += ` ${ln}${i !== languages.length - 1 ? ',' : ''}\n`; }); } content += ' // jhipster-needle-i18n-language-key-pipe - JHipster will add/remove languages in this object\n }'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /languages:.*\{([^\]]*jhipster-needle-i18n-language-key-pipe[^}]*)}/g, content, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. Language pipe not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } vueUpdateI18nConfig(languages) { const fullPath = `${this.CLIENT_MAIN_SRC_DIR}app/shared/config/config.ts`; try { // Add i18n config snippets for all languages let i18nConfig = 'const dateTimeFormats: DateTimeFormats = {\n'; if (this.enableTranslation) { languages.forEach((ln, i) => { i18nConfig += this.generateDateTimeFormat(ln, i, languages.length); }); } i18nConfig += ' // jhipster-needle-i18n-language-date-time-format - JHipster will add/remove format options in this object\n'; i18nConfig += '}'; jhipsterUtils.replaceContent( { file: fullPath, pattern: /const dateTimeFormats.*\{([^\]]*jhipster-needle-i18n-language-date-time-format[^}]*)}/g, content: i18nConfig, }, this ); } catch (e) { this.log( chalk.yellow('\nUnable to find ') + fullPath + chalk.yellow(' or missing required jhipster-needle. Language pipe not updated with languages: ') + languages + chalk.yellow(' since block was not found. Check if you have enabled translation support.\n') ); this.debug('Error:', e); } } vueUpdateLanguagesInWebpack(languages) { const fullPath = 'webpack/webpack.common.js';