UNPKG

gen-jhipster

Version:

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

1,124 lines (1,123 loc) 52.5 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 { existsSync, rmSync, statSync } from 'node:fs'; import { basename, extname, isAbsolute, join, join as joinPath, relative } from 'node:path'; import { relative as posixRelative } from 'node:path/posix'; import { requireNamespace } from '@yeoman/namespace'; import chalk from 'chalk'; import latestVersion from 'latest-version'; import { get, kebabCase, merge, mergeWith, set, snakeCase } from 'lodash-es'; import semver, { lt as semverLessThan } from 'semver'; import { simpleGit } from 'simple-git'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import YeomanGenerator, {} from 'yeoman-generator'; import { convertConfigToOption, extractArgumentsFromConfigs } from "../../lib/command/index.js"; import { packageJson } from "../../lib/index.js"; import { CRLF, LF, hasCrlf, mutateData, normalizeLineEndings, removeFieldsWithNullishValues } from "../../lib/utils/index.js"; import baseCommand from "../base/command.js"; import { dockerPlaceholderGenerator } from "../docker/utils.js"; import { GENERATOR_JHIPSTER } from "../generator-constants.js"; import { getGradleLibsVersionsProperties } from "../java-simple-application/generators/gradle/support/dependabot-gradle.js"; import { convertWriteFileSectionsToBlocks, loadConfig, loadConfigDefaults, loadDerivedConfig } from "./internal/index.js"; import { createJHipster7Context } from "./internal/jhipster7-context.js"; import { CUSTOM_PRIORITIES, PRIORITY_NAMES, PRIORITY_PREFIX, QUEUES } from "./priorities.js"; import { createNeedleCallback, joinCallbacks } from "./support/index.js"; const { INITIALIZING, PROMPTING, CONFIGURING, COMPOSING, COMPOSING_COMPONENT, LOADING, PREPARING, POST_PREPARING, DEFAULT, WRITING, POST_WRITING, INSTALL, POST_INSTALL, END, } = PRIORITY_NAMES; const asPriority = (priorityName) => `${PRIORITY_PREFIX}${priorityName}`; const relativeDir = (from, to) => { const rel = posixRelative(from, to); return rel ? `${rel}/` : ''; }; const deepMerge = (source1, source2) => mergeWith({}, source1, source2, (a, b) => (Array.isArray(a) ? [...a, ...(Array.isArray(b) ? b : [b])] : undefined)); /** * This is the base class for a generator for every generator. */ export default class CoreGenerator extends YeomanGenerator { static asPriority = asPriority; static INITIALIZING = asPriority(INITIALIZING); static PROMPTING = asPriority(PROMPTING); static CONFIGURING = asPriority(CONFIGURING); static COMPOSING = asPriority(COMPOSING); static COMPOSING_COMPONENT = asPriority(COMPOSING_COMPONENT); static LOADING = asPriority(LOADING); static PREPARING = asPriority(PREPARING); static POST_PREPARING = asPriority(POST_PREPARING); static DEFAULT = asPriority(DEFAULT); static WRITING = asPriority(WRITING); static POST_WRITING = asPriority(POST_WRITING); static INSTALL = asPriority(INSTALL); static POST_INSTALL = asPriority(POST_INSTALL); static END = asPriority(END); useVersionPlaceholders; skipChecks; ignoreNeedlesError; experimental; debugEnabled; relativeDir = relativeDir; relative = posixRelative; logger; jhipsterConfig; /** * @deprecated */ jhipsterTemplatesFolders; blueprintStorage; /** Allow to use a specific definition at current command operations */ generatorCommand; /** * @experimental * Additional commands to be considered */ generatorsToCompose = []; #jhipsterGeneratorRelativePath_; constructor(args, options, features) { super(args, options, { skipParseOptions: true, tasksMatchingPriority: true, taskPrefix: PRIORITY_PREFIX, unique: 'namespace', disableInGeneratorOptionsSupport: true, ...features, }); if (!this.options.help) { /* Force config to use 'generator-jhipster' namespace. */ this._config = this._getStorage('generator-jhipster'); /* JHipster config using proxy mode used as a plain object instead of using get/set. */ this.jhipsterConfig = this.config.createProxy(); /* Options parsing must be executed after forcing jhipster storage namespace and after sharedData have been populated */ this.#parseJHipsterConfigs(baseCommand.configs); } this.logger = this.log; if (this.options.help) { return; } this.registerPriorities(CUSTOM_PRIORITIES); const { blueprintSupport = false, queueCommandTasks = true } = this.features; // Add base template folder. this.jhipsterTemplatesFolders = [this.templatePath()]; if (!blueprintSupport && queueCommandTasks) { this.on('before:queueOwnTasks', () => { this._queueCurrentJHipsterCommandTasks(); }); } } get context() { return undefined; } /** * Override yeoman generator's usage function to fine tune --help message. */ usage() { return super.usage().replace('yo jhipster:', 'jhipster '); } /** * JHipster config with default values fallback */ get jhipsterConfigWithDefaults() { return removeFieldsWithNullishValues(this.config.getAll()); } /** * Utility method to get typed objects for autocomplete. */ asAnyTaskGroup(taskGroup) { return taskGroup; } /** * Warn or throws check failure based on current skipChecks option. * @param message */ handleCheckFailure(message) { if (this.skipChecks) { this.log.warn(message); } else { throw new Error(`${message} You can ignore this error by passing '--skip-checks' to jhipster command.`); } } /** * Wrapper for `semver.lt` to check if the oldVersion exists and is less than the newVersion. * Can be used by blueprints. */ isVersionLessThan(oldVersion, newVersion) { return oldVersion ? semverLessThan(oldVersion, newVersion) : false; } /** * Get arguments for the priority */ getArgsForPriority(_priorityName) { return [{}]; } /** * Check if the generator should ask for prompts. */ shouldAskForPrompts(_firstArg) { return true; } /** * Override yeoman-generator method that gets methods to be queued, filtering the result. */ getTaskNames() { let priorities = super.getTaskNames(); if (!this.features.disableSkipPriorities && this.options.skipPriorities) { // Make sure yeoman-generator will not throw on empty tasks due to filtered priorities. this.customLifecycle = this.customLifecycle || priorities.length > 0; priorities = priorities.filter(priorityName => !this.options.skipPriorities.includes(priorityName)); } return priorities; } _queueCurrentJHipsterCommandTasks() { this.queueTask({ queueName: QUEUES.INITIALIZING_QUEUE, taskName: 'parseCurrentCommand', cancellable: true, async method() { try { await this.#getCurrentJHipsterCommand(); } catch { return; } await this.#parseCurrentJHipsterCommand(); }, }); this.queueTask({ queueName: QUEUES.PROMPTING_QUEUE, taskName: 'promptCurrentCommand', cancellable: true, async method() { try { const command = await this.#getCurrentJHipsterCommand(); if (!command.configs) return; } catch { return; } const [firstArg] = this.getArgsForPriority(PRIORITY_NAMES.INITIALIZING); if (!this.shouldAskForPrompts(firstArg)) return; await this.#promptCurrentJHipsterCommand(); }, }); this.queueTask({ queueName: QUEUES.CONFIGURING_QUEUE, taskName: 'configureCurrentCommand', cancellable: true, async method() { try { const command = await this.#getCurrentJHipsterCommand(); if (!command.configs) return; } catch { return; } await this.#configureCurrentJHipsterCommandConfig(); }, }); this.queueTask({ queueName: QUEUES.COMPOSING_QUEUE, taskName: 'composeCurrentCommand', cancellable: true, async method() { try { await this.#getCurrentJHipsterCommand(); } catch { return; } await this.#composeCurrentJHipsterCommand(); }, }); const { loadCommand = [], skipLoadCommand } = this.features; this.queueTask({ queueName: QUEUES.LOADING_QUEUE, taskName: 'loadCurrentCommand', cancellable: true, async method() { if (!skipLoadCommand) { try { const command = await this.#getCurrentJHipsterCommand(); if (!command.configs) return; const context = this.context; loadConfig.call(this, command.configs, { application: context }); loadDerivedConfig(command.configs, { application: context }); } catch { // Ignore non existing command } const split = this.options.namespace.split(':'); if (split.length === 3 && split[2] === 'bootstrap') { const parentMeta = this.env.getGeneratorMeta(this.options.namespace.replace(':bootstrap', '')); const parentModule = await parentMeta?.importModule?.(); if (parentModule?.command?.configs) { const context = this.context; if (context) { loadConfig.call(this, parentModule.command.configs, { application: context }); loadDerivedConfig(parentModule.command.configs, { application: context }); } } } } if (loadCommand.length > 0) { const context = this.context; for (const commandToLoad of loadCommand) { if (commandToLoad.configs) { loadConfig.call(this, commandToLoad.configs, { application: context }); loadDerivedConfig(commandToLoad.configs, { application: context }); } } } }, }); this.queueTask({ queueName: QUEUES.PREPARING_QUEUE, taskName: 'preparingCurrentCommand', cancellable: true, async method() { if (!skipLoadCommand) { try { const command = await this.#getCurrentJHipsterCommand(); if (!command.configs) return; const context = this.context; loadConfigDefaults(command.configs, { context, scopes: ['blueprint', 'storage', 'context'] }); } catch { // Ignore non existing command } } if (loadCommand.length > 0) { const context = this.context; for (const commandToLoad of loadCommand) { if (commandToLoad.configs) { loadConfigDefaults(commandToLoad.configs, { context, scopes: ['blueprint', 'storage', 'context'] }); } } } }, }); } /** * Get the current Command Definition for the generator. * `generatorCommand` takes precedence. */ async #getCurrentJHipsterCommand() { if (!this.generatorCommand) { const { command } = ((await this._meta?.importModule?.()) ?? {}); if (!command) { throw new Error(`Command not found for generator ${this.options.namespace}`); } this.generatorCommand = command; return command; } return this.generatorCommand; } /** * Parse command definition arguments, options and configs. * Blueprints with command override takes precedence. */ async #parseCurrentJHipsterCommand() { const generatorCommand = await this.#getCurrentJHipsterCommand(); this.#parseJHipsterCommand(generatorCommand); } /** * Prompts for command definition configs. * Blueprints with command override takes precedence. */ async #promptCurrentJHipsterCommand() { const generatorCommand = await this.#getCurrentJHipsterCommand(); if (!generatorCommand.configs) { throw new Error(`Configs not found for generator ${this.options.namespace}`); } return this.prompt(this.#prepareQuestions(generatorCommand.configs)); } /** * Configure the current JHipster command. * Blueprints with command override takes precedence. */ async #configureCurrentJHipsterCommandConfig() { const generatorCommand = await this.#getCurrentJHipsterCommand(); if (!generatorCommand.configs) { throw new Error(`Configs not found for generator ${this.options.namespace}`); } for (const [name, def] of Object.entries(generatorCommand.configs)) { def.configure?.(this, this.options[name]); } } /** * Load the current JHipster command storage configuration into the context. * Blueprints with command override takes precedence. */ async loadCurrentJHipsterCommandConfig(context) { const generatorCommand = await this.#getCurrentJHipsterCommand(); if (!generatorCommand.configs) { throw new Error(`Configs not found for generator ${this.options.namespace}`); } loadConfig.call(this, generatorCommand.configs, { application: context }); } /** * @experimental * Compose the current JHipster command compose. * Blueprints commands compose without generators will be composed. */ async #composeCurrentJHipsterCommand() { const generatorCommand = await this.#getCurrentJHipsterCommand(); for (const compose of generatorCommand.compose ?? []) { await this.composeWithJHipster(compose); } for (const compose of this.generatorsToCompose) { await this.composeWithJHipster(compose); } } #parseJHipsterCommand(commandDef) { if (commandDef.arguments) { this._parseJHipsterArguments(commandDef.arguments); } else if (commandDef.configs) { this._parseJHipsterArguments(extractArgumentsFromConfigs(commandDef.configs)); } if (commandDef.configs) { this.#parseJHipsterConfigs(commandDef.configs); } } #parseJHipsterConfigs(configs = {}, common = false) { Object.entries(configs).forEach(([optionName, configDesc]) => { const optionsDesc = convertConfigToOption(optionName, configDesc); if (!optionsDesc || !optionsDesc.type || (common && configDesc.scope === 'generator')) return; let optionValue; const { name, type } = optionsDesc; const envName = configDesc.cli?.env; // Hidden options are test options, which doesn't rely on commander for options parsing. // We must parse environment variables manually if (this.options[name] === undefined && envName && process.env[envName]) { optionValue = process.env[envName]; } else { optionValue = this.options[name]; } if (optionValue !== undefined) { optionValue = type !== Array && type !== Function ? type(optionValue) : optionValue; switch (optionsDesc.scope) { case 'storage': { this.config.set(optionName, optionValue); break; } case 'blueprint': { if (!this.blueprintStorage) { throw new Error('Blueprint storage is not initialized'); } this.blueprintStorage.set(optionName, optionValue); break; } case 'generator': { this[optionName] = optionValue; break; } case 'context': { this.context[optionName] = optionValue; break; } default: { if (optionsDesc.scope !== 'none') { throw new Error(`Scope ${optionsDesc.scope} not supported`); } } } } else if (optionsDesc.default !== undefined && optionsDesc.scope === 'generator' && this[optionName] === undefined) { this[optionName] = optionsDesc.default; } }); } _parseJHipsterArguments(jhipsterArguments = {}) { const hasPositionalArguments = Boolean(this.options.positionalArguments); let positionalArguments = hasPositionalArguments ? this.options.positionalArguments : this._args; const argumentEntries = Object.entries(jhipsterArguments); if (hasPositionalArguments && positionalArguments.length > argumentEntries.length) { throw new Error('More arguments than allowed'); } argumentEntries.find(([argumentName, argumentDef]) => { if (positionalArguments.length > 0) { let argument; if (hasPositionalArguments || argumentDef.type !== Array) { // Positional arguments already parsed or a single argument. argument = Array.isArray(positionalArguments) ? positionalArguments.shift() : positionalArguments; } else { // Varargs argument. argument = positionalArguments; positionalArguments = []; } // Replace varargs empty array with undefined. argument = Array.isArray(argument) && argument.length === 0 ? undefined : argument; if (argument !== undefined) { const convertedValue = !argumentDef.type || argumentDef.type === Array ? argument : argumentDef.type(argument); switch (argumentDef.scope) { case undefined: case 'generator': { this[argumentName] = convertedValue; break; } case 'context': { this.context[argumentName] = convertedValue; break; } case 'storage': { this.config.set(argumentName, convertedValue); break; } case 'blueprint': { if (!this.blueprintStorage) { throw new Error('Blueprint storage is not initialized'); } this.blueprintStorage.set(argumentName, convertedValue); break; } } } } else { if (argumentDef.required) { throw new Error(`Missing required argument ${argumentName}`); } return true; } return false; }); // Arguments should only be parsed by the root generator, cleanup to don't be forwarded. this.options.positionalArguments = []; } #prepareQuestions(configs = {}) { return Object.entries(configs) .filter(([_name, def]) => def?.prompt) .map(([name, def]) => { let promptSpec = typeof def.prompt === 'function' ? def.prompt(this, def) : { ...def.prompt }; let storage; switch (def.scope) { case 'storage': case undefined: { storage = this.config; if (promptSpec.default === undefined) { promptSpec = { ...promptSpec, default: () => this.jhipsterConfigWithDefaults[name] }; } break; } case 'blueprint': { if (!this.blueprintStorage) { throw new Error('Blueprint storage is not initialized'); } storage = this.blueprintStorage; break; } case 'generator': { storage = { getPath: (path) => get(this, path), setPath: (path, value) => set(this, path, value), }; break; } case 'context': { storage = { getPath: (path) => get(this.context, path), setPath: (path, value) => set(this.context, path, value), }; break; } } return { name, choices: def.choices, ...promptSpec, storage, }; }); } get #jhipsterGeneratorRelativePath() { if (!this.#jhipsterGeneratorRelativePath_) { try { this.#jhipsterGeneratorRelativePath_ = requireNamespace(this.options.namespace).generator.replace(':', '/generators/'); } catch { throw new Error('Could not determine the generator name'); } } return this.#jhipsterGeneratorRelativePath_; } /** * Alternative templatePath that fetches from the blueprinted generator, instead of the blueprint. */ jhipsterTemplatePath(...path) { return this.fetchFromInstalledJHipster(this.#jhipsterGeneratorRelativePath, 'templates', ...path); } /** * Returns the resources path in the blueprinted jhipster generator */ jhipsterResourcesPath(...path) { return this.fetchFromInstalledJHipster(this.#jhipsterGeneratorRelativePath, 'resources', ...path); } /** * Reads a resource file from the generator */ readResource(path) { return this.fs.read(this.resourcesPath(path)); } /** * Join a path to the source root. * @param dest - path parts * @return joined path */ resourcesPath(...dest) { const filepath = join(...dest); if (isAbsolute(filepath)) { return filepath; } return this.templatePath('../resources', filepath); } /** * Reads a resource file from the blueprinted jhipster generator */ readJHipsterResource(path) { return this.fs.read(this.jhipsterResourcesPath(path)); } async dependsOnJHipster(generator, options) { return this.composeWithJHipster(generator, { ...options, schedule: false, }); } /** * Compose with a jhipster bootstrap generator using default jhipster config, but queue it immediately. */ dependsOnBootstrap(gen, options) { return this.dependsOnJHipster(`jhipster:${gen}:bootstrap`, options); } async composeWithJHipster(gen, options) { assert(typeof gen === 'string', 'generator should to be a string'); let generator = gen; if (!isAbsolute(generator)) { const namespace = generator.includes(':') ? generator : `jhipster:${generator}`; if (await this.env.get(namespace)) { generator = namespace; } else { throw new Error(`Generator ${generator} was not found`); } } return this.composeWith(generator, { forwardOptions: false, ...options, generatorOptions: { ...this.options, positionalArguments: undefined, ...options?.generatorOptions, }, }); } /** * Remove File */ removeFile(...path) { const destinationFile = this.destinationPath(...path); const relativePath = relative(this.env.logCwd, destinationFile); // Delete from memory fs to keep updated. this.fs.delete(destinationFile); try { if (destinationFile && statSync(destinationFile).isFile()) { this.log.info(`Removing legacy file ${relativePath}`); rmSync(destinationFile, { force: true }); } } catch { this.log.info(`Could not remove legacy file ${relativePath}`); } return destinationFile; } /** * Remove Folder * @param path */ removeFolder(...path) { const destinationFolder = this.destinationPath(...path); const relativePath = relative(this.env.logCwd, destinationFolder); // Delete from memory fs to keep updated. this.fs.delete(`${destinationFolder}/**`); try { if (statSync(destinationFolder).isDirectory()) { this.log.info(`Removing legacy folder ${relativePath}`); rmSync(destinationFolder, { recursive: true }); } } catch { this.log.log(`Could not remove folder ${destinationFolder}`); } } /** * Fetch files from the generator-jhipster instance installed */ fetchFromInstalledJHipster(...path) { if (path) { return joinPath(import.meta.dirname, '..', ...path); } return path; } /** * Utility function to write file. * * @param source * @param destination - destination * @param data - template data * @param copyOptions */ writeFile(source, destination, data = this, copyOptions = {}) { // Convert to any because ejs types doesn't support string[] https://github.com/DefinitelyTyped/DefinitelyTyped/pull/63315 const root = this.jhipsterTemplatesFolders ?? this.templatePath(); try { return this.renderTemplate(source, destination, data, { noGlob: true, ...copyOptions, transformOptions: { root, ...copyOptions.transformOptions }, }); } catch (error) { throw new Error(`Error writing file ${source} to ${destination}: ${error}`, { cause: error }); } } /** * write the given files using provided options. */ async writeFiles(options) { const paramCount = Object.keys(options).filter(key => ['sections', 'blocks', 'templates'].includes(key)).length; assert(paramCount > 0, 'One of sections, blocks or templates is required'); assert(paramCount === 1, 'Only one of sections, blocks or templates must be provided'); let { context: templateData = {} } = options; const { rootTemplatesPath, customizeTemplatePath = file => file, transform: methodTransform = [] } = options; const startTime = new Date().getMilliseconds(); const { customizeTemplatePaths: contextCustomizeTemplatePaths = [] } = templateData; const { jhipster7Migration } = this.features; if (jhipster7Migration) { templateData = createJHipster7Context(this, options.context ?? {}, { log: jhipster7Migration === 'verbose' ? (msg) => this.log.info(msg) : () => { }, }); } /* Build lookup order first has preference. * Example * rootTemplatesPath = ['reactive', 'common'] * jhipsterTemplatesFolders = ['/.../generator-jhipster-blueprint/server/templates', '/.../generator-jhipster/server/templates'] * * /.../generator-jhipster-blueprint/server/templates/reactive/templatePath * /.../generator-jhipster-blueprint/server/templates/common/templatePath * /.../generator-jhipster/server/templates/reactive/templatePath * /.../generator-jhipster/server/templates/common/templatePath */ let rootTemplatesAbsolutePath; if (!rootTemplatesPath) { rootTemplatesAbsolutePath = this.jhipsterTemplatesFolders; } else if (typeof rootTemplatesPath === 'string' && isAbsolute(rootTemplatesPath)) { rootTemplatesAbsolutePath = rootTemplatesPath; } else { rootTemplatesAbsolutePath = this.jhipsterTemplatesFolders.flatMap(templateFolder => (Array.isArray(rootTemplatesPath) ? rootTemplatesPath : [rootTemplatesPath]).map(relativePath => join(templateFolder, relativePath))); } const normalizeEjs = (file) => file.replace('.ejs', ''); const resolveCallback = (maybeCallback, fallback) => { if (maybeCallback === undefined) { if (typeof fallback === 'function') { return resolveCallback(fallback); } return fallback; } if (typeof maybeCallback === 'boolean' || typeof maybeCallback === 'string') { return maybeCallback; } if (typeof maybeCallback === 'function') { return maybeCallback.call(this, templateData) || false; } throw new Error(`Type not supported ${maybeCallback}`); }; const renderTemplate = async ({ condition, sourceFile, destinationFile, options, noEjs, transform, binary, }) => { if (condition !== undefined && !resolveCallback(condition, true)) { return undefined; } const extension = extname(sourceFile); const isBinary = binary || ['.png', '.jpg', '.gif', '.svg', '.ico'].includes(extension); const appendEjs = noEjs === undefined ? !isBinary && extension !== '.ejs' : !noEjs; let targetFile; if (typeof destinationFile === 'function') { targetFile = resolveCallback(destinationFile); } else { targetFile = appendEjs ? normalizeEjs(destinationFile) : destinationFile; } let sourceFileFrom; if (Array.isArray(rootTemplatesAbsolutePath)) { // Look for existing templates let existingTemplates = rootTemplatesAbsolutePath .map(rootPath => this.templatePath(rootPath, sourceFile)) .filter(templateFile => existsSync(appendEjs ? `${templateFile}.ejs` : templateFile)); if (existingTemplates.length === 0 && jhipster7Migration) { existingTemplates = rootTemplatesAbsolutePath .map(rootPath => this.templatePath(rootPath, appendEjs ? sourceFile : `${sourceFile}.ejs`)) .filter(templateFile => existsSync(templateFile)); } if (existingTemplates.length > 1) { const moreThanOneMessage = `Multiples templates were found for file ${sourceFile}, using the first templates: ${JSON.stringify(existingTemplates, null, 2)}`; if (existingTemplates.length > 2) { this.log.warn(`Possible blueprint conflict detected: ${moreThanOneMessage}`); } else { this.log.debug(moreThanOneMessage); } } sourceFileFrom = existingTemplates.shift(); } else if (typeof rootTemplatesAbsolutePath === 'string') { sourceFileFrom = this.templatePath(rootTemplatesAbsolutePath, sourceFile); } else { sourceFileFrom = this.templatePath(sourceFile); } const file = customizeTemplatePath.call(this, { sourceFile, resolvedSourceFile: sourceFileFrom, destinationFile: targetFile }); if (!file) { return undefined; } sourceFileFrom = file.resolvedSourceFile; targetFile = file.destinationFile; let templatesRoots = Array.isArray(rootTemplatesAbsolutePath) ? [...rootTemplatesAbsolutePath] : [rootTemplatesAbsolutePath]; for (const contextCustomizeTemplatePath of contextCustomizeTemplatePaths) { const file = contextCustomizeTemplatePath.call(this, { namespace: this.options.namespace, sourceFile, resolvedSourceFile: sourceFileFrom, destinationFile: targetFile, templatesRoots, }, templateData); if (!file) { return undefined; } sourceFileFrom = file.resolvedSourceFile; targetFile = file.destinationFile; templatesRoots = file.templatesRoots; } if (sourceFileFrom === undefined) { throw new Error(`Template file ${sourceFile} was not found at ${rootTemplatesAbsolutePath}`); } try { if (!appendEjs && extname(sourceFileFrom) !== '.ejs') { await this.copyTemplateAsync(sourceFileFrom, targetFile); } else { let useAsync = true; if (templateData.entityClass) { if (!templateData.baseName) { throw new Error('baseName is required at templates context'); } const sourceBasename = basename(sourceFileFrom); this.emit('before:render', sourceBasename, templateData); // Async calls will make the render method to be scheduled, allowing the faker key to change in the meantime. useAsync = false; } const transformOptions = { ...options?.renderOptions, // Set root for ejs to lookup for partials. root: templatesRoots, // multiple roots causes ejs caching issues due to cache key issues. cache: templatesRoots.length === 1, }; const copyOptions = { noGlob: true, transformOptions }; if (appendEjs) { sourceFileFrom = `${sourceFileFrom}.ejs`; } if (noEjs && useAsync) { await this.copyTemplateAsync(sourceFileFrom, targetFile, copyOptions); } else if (noEjs) { this.copyTemplate(sourceFileFrom, targetFile, copyOptions); } else if (useAsync) { await this.renderTemplateAsync(sourceFileFrom, targetFile, templateData, copyOptions); } else { this.renderTemplate(sourceFileFrom, targetFile, templateData, copyOptions); } } } catch (error) { throw new Error(`Error rendering template ${sourceFileFrom} to ${targetFile}: ${error}`, { cause: error }); } if (!isBinary && transform?.length) { this.editFile(targetFile, ...transform); } return targetFile; }; let parsedTemplates; if ('sections' in options || 'blocks' in options) { const sectionTransform = 'sections' in options ? (options.sections._?.transform ?? []) : []; parsedTemplates = ('sections' in options ? convertWriteFileSectionsToBlocks(options.sections) : options.blocks) .flatMap((block, blockIdx) => { const { path: blockPathValue = './', from: blockFromCallback, to: blockToCallback, condition: blockConditionCallback, transform: blockTransform = [], renameTo: blockRenameTo, } = block; // Temporary variable added to identify section/block const blockSpecPath = 'blockSpecPath' in block ? block.blockSpecPath : `${blockIdx}`; assert(typeof block === 'object', `Block must be an object for ${blockSpecPath}`); assert(Array.isArray(block.templates), `Block templates must be an array for ${blockSpecPath}`); const condition = resolveCallback(blockConditionCallback); if (condition !== undefined && !condition) { return undefined; } if (typeof blockPathValue === 'function') { throw new Error(`Block path should be static for ${blockSpecPath}`); } const blockPath = resolveCallback(blockFromCallback, blockPathValue); const blockTo = resolveCallback(blockToCallback, blockPath) || blockPath; return block.templates.map((fileSpec, fileIdx) => { const fileSpecPath = `${blockSpecPath}[${fileIdx}]`; assert(typeof fileSpec === 'object' || typeof fileSpec === 'string' || typeof fileSpec === 'function', `File must be an object, a string or a function for ${fileSpecPath}`); if (typeof fileSpec === 'function') { fileSpec = fileSpec.call(this, templateData); } let noEjs; let derivedTransform; if (typeof blockTransform === 'boolean') { noEjs = !blockTransform; derivedTransform = [...methodTransform, ...sectionTransform]; } else { derivedTransform = [...methodTransform, ...sectionTransform, ...blockTransform]; } if (typeof fileSpec === 'string') { const sourceFile = join(blockPath, fileSpec); let destinationFile; if (blockRenameTo) { destinationFile = this.destinationPath(blockRenameTo.call(this, templateData, fileSpec)); } else { destinationFile = this.destinationPath(blockTo, fileSpec); } return { sourceFile, destinationFile, noEjs, transform: derivedTransform }; } const { condition, options, file, renameTo, transform: fileTransform = [], binary } = fileSpec; let { sourceFile, destinationFile } = fileSpec; if (typeof fileTransform === 'boolean') { noEjs = !fileTransform; } else if (Array.isArray(fileTransform)) { derivedTransform = [...derivedTransform, ...fileTransform]; } else if (fileTransform !== undefined) { throw new Error(`Transform ${fileTransform} value is not supported`); } const normalizedFile = resolveCallback(sourceFile || file); sourceFile = join(blockPath, normalizedFile); destinationFile = join(resolveCallback(destinationFile || renameTo, normalizedFile)); if (blockRenameTo) { destinationFile = this.destinationPath(blockRenameTo.call(this, templateData, destinationFile)); } else { destinationFile = this.destinationPath(blockTo, destinationFile); } const override = resolveCallback(fileSpec.override); if (override !== undefined && !override && this.fs.exists(destinationFile.replace(/\.jhi$/, ''))) { this.log.debug(`skipping file ${destinationFile}`); return undefined; } return { condition, sourceFile, destinationFile, options, transform: derivedTransform, noEjs, binary, }; }); }) .filter(Boolean); } else { parsedTemplates = options.templates.map(template => { if (typeof template === 'string') { return { sourceFile: template, destinationFile: template }; } return template; }); } const files = (await Promise.all(parsedTemplates.map(template => renderTemplate(template)).filter(Boolean))); this.log.debug(`Time taken to write files: ${new Date().getMilliseconds() - startTime}ms`); return files.filter(Boolean); } editFile(file, options, ...transformCallbacks) { let actualOptions; if (typeof options === 'function') { transformCallbacks = [options, ...transformCallbacks]; actualOptions = {}; } else if (options === undefined) { actualOptions = {}; } else if ('needle' in options && 'contentToAdd' in options) { transformCallbacks = [createNeedleCallback(options), ...transformCallbacks]; actualOptions = {}; } else { actualOptions = options; } let filePath = this.destinationPath(file); if (!this.env.sharedFs.existsInMemory(filePath) && this.env.sharedFs.existsInMemory(`${filePath}.jhi`)) { filePath = `${filePath}.jhi`; } let originalContent; try { originalContent = this.readDestination(filePath); } catch { // null return should be treated like an error. } if (typeof originalContent !== 'string') { const { ignoreNonExisting, create } = actualOptions; const errorMessage = typeof ignoreNonExisting === 'string' ? ` ${ignoreNonExisting}.` : ''; if (!create || transformCallbacks.length === 0) { if (ignoreNonExisting || this.ignoreNeedlesError) { this.log(`${chalk.yellow('\nUnable to find ')}${filePath}.${chalk.yellow(errorMessage)}\n`); // return a noop. const noop = () => noop; return noop; } throw new Error(`Unable to find ${filePath}. ${errorMessage}`); } // allow to edit non existing files originalContent = ''; } let newContent = originalContent; const writeCallback = (...callbacks) => { const { autoCrlf = this.jhipsterConfigWithDefaults.autoCrlf, assertModified } = actualOptions; try { const fileHasCrlf = autoCrlf && hasCrlf(newContent); newContent = joinCallbacks(...callbacks).call(this, fileHasCrlf ? normalizeLineEndings(newContent, LF) : newContent, filePath); if (assertModified && originalContent === newContent) { const errorMessage = `${chalk.yellow('Fail to modify ')}${filePath}.`; if (!this.ignoreNeedlesError) { throw new Error(errorMessage); } this.log(errorMessage); } this.writeDestination(filePath, fileHasCrlf ? normalizeLineEndings(newContent, CRLF) : newContent); } catch (error) { if (error instanceof Error) { throw new Error(`Error editing file ${filePath}: ${error.message} at ${error.stack}`); } throw new Error(`Unknown Error ${error}`); } return writeCallback; }; return writeCallback(...transformCallbacks); } /** * Convert value to a yaml and write to destination */ writeDestinationYaml(filepath, value) { this.writeDestination(filepath, stringifyYaml(value)); } /** * Merge value to an existing yaml and write to destination * Removes every comment (due to parsing/merging process) except the at the top of the file. */ mergeDestinationYaml(filepath, value) { this.editFile(filepath, content => { const lines = content.split('\n'); const headerComments = []; lines.find(line => { if (line.startsWith('#')) { headerComments.push(line); return false; } return true; }); const mergedContent = stringifyYaml(deepMerge(parseYaml(content), value)); const header = headerComments.length > 0 ? headerComments.join('\n').concat('\n') : ''; return `${header}${mergedContent}`; }); } /** * Merge value to an existing json and write to destination */ mergeDestinationJson(filepath, value) { this.editFile(filepath, { create: true }, content => JSON.stringify(merge(content ? JSON.parse(content) : {}, value), null, 2)); } /** * Shallow clone or convert dependencies to placeholder if needed. */ prepareDependencies(map, valuePlaceholder) { let placeholder; if (valuePlaceholder === 'java') { placeholder = value => `'${kebabCase(value).toUpperCase()}-VERSION'`; } else if (valuePlaceholder === 'docker') { placeholder = dockerPlaceholderGenerator; } else { placeholder = valuePlaceholder ?? (value => `${snakeCase(value).toUpperCase()}_VERSION`); } if (this.useVersionPlaceholders) { return Object.fromEntries(Object.keys(map).map(dep => [dep, placeholder(dep)])); } return { ...map, }; } loadNodeDependencies(destination, source) { mutateData(destination, this.prepareDependencies(source)); } loadJavaDependenciesFromGradleCatalog(javaDependencies, gradleCatalog) { if (typeof gradleCatalog !== 'string') { const tomlFile = '../resources/gradle/libs.versions.toml'; gradleCatalog = gradleCatalog ? this.jhipsterTemplatePath(tomlFile) : this.templatePath(tomlFile); } const gradleLibsVersions = this.readTemplate(gradleCatalog)?.toString(); if (gradleLibsVersions) { Object.assign(javaDependencies, this.prepareDependencies(getGradleLibsVersionsProperties(gradleLibsVersions), 'java')); } } readResourcesPackageJson(packageJsonFile = 'package.json') { packageJsonFile = this.resourcesPath(packageJsonFile); const packageJson = this.fs.readJSON(packageJsonFile, {}); return { ...packageJson, devDependencies: { ...packageJson.devDependencies }, dependencies: { ...packageJson.dependencies } }; } loadNodeDependenciesFromPackageJson(destination, packageJsonFile) { const { devDependencies, dependencies } = this.readResourcesPackageJson(packageJsonFile); this.loadNodeDependencies(destination, { ...devDependencies, ...dependencies }); } /** * Print ValidationResult info/warnings or throw result Error. */ validateResult(result, { throwOnError = true } = {}) { // Don't print check info by default for cleaner outputs. if (result.debug) { if (Array.isArray(result.debug)) { for (const debug of result.debug) { this.log.debug(debug); }