gen-jhipster
Version:
VHipster - Spring Boot + Angular/React/Vue in one handy generator
1,124 lines (1,123 loc) • 52.5 kB
JavaScript
/**
* Copyright 2013-2026 the original author or authors from the JHipster project.
*
* This file is part of the JHipster project, see https://www.jhipster.tech/
* for more information.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import assert from 'node:assert';
import { 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);
}