UNPKG

gen-jhipster

Version:

VHipster - Spring Boot + Angular/React/Vue in one handy generator

706 lines (705 loc) 30.7 kB
/** * 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 { }