generator-jhipster
Version:
Spring Boot + Angular/React/Vue in one handy generator
1,114 lines • 54.6 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 { getCommandBlueprintLoadingMutations, getCommandDefaultMutations, getCommandDerivedPropertyMutations, } from "../../lib/command/mutations.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 } 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', { transform: this.features.configTransform });
/* 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 #commandsToLoad() {
return this.getContextData('jhipster:loadedNamespaces', {
factory: () => new Set(),
});
}
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 = true } = this.features;
if (skipLoadCommand) {
return;
}
const commandWithConfigs = (command) => !!command.configs;
const mergeCommandsConfigs = (commands) => {
const mergedConfigs = {};
for (const command of commands.filter(commandWithConfigs)) {
Object.assign(mergedConfigs, command.configs);
}
return mergedConfigs;
};
const collectNamespaceCommands = async ({ loadImports }, ...namespaces) => {
const result = [];
for (let namespace of namespaces) {
namespace = namespace.includes(':') ? namespace : `jhipster:${namespace}`;
const commandsToLoad = this.#commandsToLoad;
if (!commandsToLoad.has(namespace)) {
commandsToLoad.add(namespace);
const commandMeta = this.env.getGeneratorMeta(namespace);
const commandModule = await commandMeta?.importModule?.();
const command = commandModule?.command;
if (command) {
result.push(command);
if (loadImports) {
result.push(...(await collectNamespaceCommands({ loadImports: true }, ...(command.import ?? []))));
}
}
}
}
return result;
};
const directCommands = [];
let directCommandsMergedConfigs = {};
this.queueTask({
queueName: QUEUES.LOADING_QUEUE,
taskName: 'loadingCommand',
cancellable: true,
async method() {
try {
const commandsToLoad = this.#commandsToLoad;
if (!commandsToLoad.has(this.options.namespace)) {
const command = await this.#getCurrentJHipsterCommand();
if (command.configs) {
commandsToLoad.add(this.options.namespace);
directCommands.push(command);
}
else {
directCommands.push(...(await collectNamespaceCommands({ loadImports: false }, this.options.namespace)));
}
}
}
catch {
// Ignore non existing command
}
const split = this.options.namespace.split(':');
if (split.length === 3 && split[2] === 'bootstrap') {
const mainNamespace = this.options.namespace.replace(':bootstrap', '');
directCommands.push(...(await collectNamespaceCommands({ loadImports: false }, mainNamespace)));
}
directCommandsMergedConfigs = mergeCommandsConfigs(directCommands);
if (this.blueprintStorage) {
mutateData(this.context, getCommandBlueprintLoadingMutations(this, directCommandsMergedConfigs));
}
},
});
this.queueTask({
queueName: QUEUES.PREPARING_QUEUE,
taskName: 'preparingCurrentCommand',
cancellable: true,
async method() {
mutateData(this.context, getCommandDefaultMutations(directCommandsMergedConfigs), getCommandDerivedPropertyMutations(directCommandsMergedConfigs));
const imports = directCommands.flatMap(command => command.import ?? []);
if (imports.length > 0 || loadCommand.length > 0) {
// Populate imported commands and loadCommand, to allow the generator itself to load them.
this.queueTask({
queueName: QUEUES.PREPARING_QUEUE,
taskName: 'preparingImportedCommand',
cancellable: true,
async method() {
const importedCommandNamespaces = [
...loadCommand.filter(l => typeof l === 'string'),
...loadCommand.filter(l => typeof l === 'object').flatMap(l => l.import ?? []),
...imports,
];
const importedCommands = [
...loadCommand.filter(l => typeof l === 'object'),
...(await collectNamespaceCommands({ loadImports: true }, ...importedCommandNamespaces)),
];
const mergedImportedConfigs = mergeCommandsConfigs(importedCommands);
mutateData(this.context, getCommandDefaultMutations(mergedImportedConfigs), getCommandDerivedPropertyMutations(mergedImportedConfigs));
},
});
}
},
});
}
/**
* 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) {
// @ts-expect-error removed property.
if (!this.skipChecks && commandDef.options) {
throw new Error('Options are not supported anymore in JHipster commands, please use configs instead');
}
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 = {}) {
Object.entries(configs).forEach(([optionName, configDesc]) => {
const optionsDesc = convertConfigToOption(optionName, configDesc);
if (!optionsDesc?.type)
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, {
allowPackageOnlyNamespace: false,
}).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 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 templateData = options.context ?? {};
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, templateData, {
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) => {
if (typeof maybeCallback === 'function') {
return resolveCallback(maybeCallback.call(this, templateData));
}
return maybeCallback;
};
const renderTemplate = async ({ condition, sourceFile, destinationFile, options, noEjs, transform, binary, }) => {
if (condition !== undefined && !resolveCallback(condition)) {
return undefined;
}
sourceFile = resolveCallback(sourceFile);
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);
}
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}`);
if (blockConditionCallback !== undefined && !resolveCallback(blockConditionCallback)) {
return undefined;
}
if (typeof blockPathValue === 'function') {
throw new TypeError(`Block path should be static for ${blockSpecPath}`);
}
const blockPath = resolveCallback(blockFromCallback) ?? resolveCallback(blockPathValue);
const blockTo = resolveCallback(blockToCallback) ?? resolveCallback(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) ?? resolveCallback(file);
if (normalizedFile === undefined) {
throw new Error(`sourceFile is required for ${fileSpecPath}`);
}
sourceFile = join(blockPath, normalizedFile);
destinationFile = resolveCallback(destinationFile) ?? resolveCallback(renameTo) ?? normalizedFile;
if (blockRenameTo) {
destinationFile = this.destinationPath(blockRenameTo.call(this, templateData, destinationFile));
}
else {
destinationFile = this.destinationPath(blockTo, destinationFile);
}
if (fileSpec.override !== undefined &&
!resolveCallback(fileSpec.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 editing 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 = dockerPlacehold