gen-jhipster
Version:
Spring Boot + Angular/React/Vue in one handy generator
1,107 lines (1,106 loc) • 55.7 kB
JavaScript
/**
* Copyright 2013-2024 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 { basename, dirname, extname, isAbsolute, join, join as joinPath, relative } from 'path';
import { relative as posixRelative } from 'path/posix';
import { createHash } from 'crypto';
import { fileURLToPath } from 'url';
import { existsSync, readFileSync, rmSync, statSync } from 'fs';
import assert from 'assert';
import { requireNamespace } from '@yeoman/namespace';
import chalk from 'chalk';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { defaults, get, kebabCase, merge, mergeWith, set, snakeCase } from 'lodash-es';
import { simpleGit } from 'simple-git';
import semver, { lt as semverLessThan } from 'semver';
import YeomanGenerator from 'yeoman-generator';
import latestVersion from 'latest-version';
import SharedData from '../base/shared-data.js';
import { CUSTOM_PRIORITIES, PRIORITY_NAMES, PRIORITY_PREFIX, QUEUES } from '../base/priorities.js';
import { createJHipster7Context, formatDateForChangelog, joinCallbacks, removeFieldsWithNullishValues } from '../base/support/index.js';
import { convertConfigToOption, } from '../../lib/command/index.js';
import { packageJson } from '../../lib/index.js';
import { GENERATOR_BOOTSTRAP } from '../generator-list.js';
import NeedleApi from '../needle-api.js';
import baseCommand from '../base/command.js';
import { GENERATOR_JHIPSTER, YO_RC_FILE } from '../generator-constants.js';
import { loadConfig, loadDerivedConfig } from '../../lib/internal/index.js';
import { getGradleLibsVersionsProperties } from '../gradle/support/dependabot-gradle.js';
import { dockerPlaceholderGenerator } from '../docker/utils.js';
import { getConfigWithDefaults } from '../../lib/jhipster/index.js';
import { extractArgumentsFromConfigs } from '../../lib/command/index.js';
const { INITIALIZING, PROMPTING, CONFIGURING, COMPOSING, COMPOSING_COMPONENT, LOADING, PREPARING, POST_PREPARING, DEFAULT, WRITING, POST_WRITING, INSTALL, POST_INSTALL, END, } = PRIORITY_NAMES;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
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.concat(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);
context;
useVersionPlaceholders;
skipChecks;
ignoreNeedlesError;
experimental;
debugEnabled;
jhipster7Migration;
relativeDir = relativeDir;
relative = posixRelative;
sharedData;
logger;
jhipsterConfig;
/**
* @deprecated
*/
jhipsterTemplatesFolders;
blueprintStorage;
/** Allow to use a specific definition at current command operations */
generatorCommand;
/**
* @experimental
* Additional commands to be considered
*/
generatorsToCompose = [];
_jhipsterGenerator;
_needleApi;
constructor(args, options, features) {
super(args, options, {
skipParseOptions: true,
tasksMatchingPriority: true,
taskPrefix: PRIORITY_PREFIX,
unique: 'namespace',
...features,
});
if (!this.options.help) {
/* Force config to use 'gen-jhipster' namespace. */
this._config = this._getStorage('gen-jhipster');
/* JHipster config using proxy mode used as a plain object instead of using get/set. */
this.jhipsterConfig = this.config.createProxy();
this.sharedData = this.createSharedData({ help: this.options.help });
/* Options parsing must be executed after forcing jhipster storage namespace and after sharedData have been populated */
this.parseJHipsterOptions(baseCommand.options);
// Don't write jhipsterVersion to .yo-rc.json when reproducible
if (this.options.namespace.startsWith('jhipster:') &&
!this.options.namespace.startsWith('jhipster:bootstrap') &&
this.getFeatures().storeJHipsterVersion !== false &&
!this.options.reproducibleTests &&
!this.jhipsterConfig.jhipsterVersion) {
this.storeCurrentJHipsterVersion();
}
}
this.logger = this.log;
if (this.options.help) {
return;
}
this.registerPriorities(CUSTOM_PRIORITIES);
if (this.getFeatures().jhipsterBootstrap ?? true) {
// jhipster:bootstrap is always required. Run it once the environment starts.
this.env.queueTask('environment:run', async () => this.composeWithJHipster(GENERATOR_BOOTSTRAP).then(), {
once: 'queueJhipsterBootstrap',
startQueue: false,
});
}
// Add base template folder.
this.jhipsterTemplatesFolders = [this.templatePath()];
this.jhipster7Migration = this.features.jhipster7Migration ?? false;
if (this.features.queueCommandTasks === true) {
this.on('before:queueOwnTasks', () => {
this.queueCurrentJHipsterCommandTasks();
});
}
}
/**
* Override yeoman generator's usage function to fine tune --help message.
*/
usage() {
return super.usage().replace('yo jhipster:', 'jhipster ');
}
storeCurrentJHipsterVersion() {
this.jhipsterConfig.jhipsterVersion = packageJson.version;
}
/**
* @deprecated
*/
get needleApi() {
if (this._needleApi === undefined || this._needleApi === null) {
this._needleApi = new NeedleApi(this);
}
return this._needleApi;
}
/**
* JHipster config with default values fallback
*/
get jhipsterConfigWithDefaults() {
const configWithDefaults = getConfigWithDefaults(removeFieldsWithNullishValues(this.config.getAll()));
defaults(configWithDefaults, {
skipFakeData: false,
skipCheckLengthOfIdentifier: false,
enableGradleEnterprise: false,
pages: [],
});
return configWithDefaults;
}
/**
* 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.`);
}
}
/**
* Check if the JHipster version used to generate an existing project is less than the passed version argument
*
* @param {string} version - A valid semver version string
*/
isJhipsterVersionLessThan(version) {
const jhipsterOldVersion = this.sharedData.getControl().jhipsterOldVersion;
return this.isVersionLessThan(jhipsterOldVersion, version);
}
/**
* 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) {
const control = this.sharedData.getControl();
if (priorityName === POST_WRITING || priorityName === PREPARING || priorityName === POST_PREPARING) {
const source = this.sharedData.getSource();
return [{ control, source }];
}
if (priorityName === WRITING) {
if (existsSync(this.destinationPath(YO_RC_FILE))) {
try {
const oldConfig = JSON.parse(readFileSync(this.destinationPath(YO_RC_FILE)).toString())[GENERATOR_JHIPSTER];
const newConfig = this.config.getAll();
const keys = [...new Set([...Object.keys(oldConfig), ...Object.keys(newConfig)])];
const configChanges = Object.fromEntries(keys
.filter(key => Array.isArray(newConfig[key])
? newConfig[key].length === oldConfig[key].length &&
newConfig[key].find((element, index) => element !== oldConfig[key][index])
: newConfig[key] !== oldConfig[key])
.map(key => [key, { newValue: newConfig[key], oldValue: oldConfig[key] }]));
return [{ control, configChanges }];
}
catch {
// Fail to parse
}
}
}
return [{ control }];
}
/**
* Check if the generator should ask for prompts.
*/
shouldAskForPrompts({ control }) {
return !control.existingProject || this.options.askAnswered === 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 taskArgs = this.getArgsForPriority(PRIORITY_NAMES.INITIALIZING);
const [{ control }] = taskArgs;
if (!control)
throw new Error(`Control object not found in ${this.options.namespace}`);
if (!this.shouldAskForPrompts({ control }))
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();
},
});
this.queueTask({
queueName: QUEUES.LOADING_QUEUE,
taskName: 'loadCurrentCommand',
cancellable: true,
async method() {
try {
const command = await this.getCurrentJHipsterCommand();
if (!command.configs)
return;
const taskArgs = this.getArgsForPriority(PRIORITY_NAMES.LOADING);
const [{ application }] = taskArgs;
loadConfig.call(this, command.configs, { application: application ?? this });
loadDerivedConfig(command.configs, { application });
}
catch {
// Ignore non existing command
}
},
});
}
/**
* 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 def of Object.values(generatorCommand.configs)) {
def.configure?.(this);
}
}
/**
* 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.options || commandDef.configs) {
this.parseJHipsterOptions(commandDef.options, commandDef.configs);
}
}
parseJHipsterOptions(options, configs = {}, common = false) {
if (typeof configs === 'boolean') {
common = configs;
configs = {};
}
Object.entries(options ?? {})
.concat(Object.entries(configs).map(([name, def]) => [name, convertConfigToOption(name, def)]))
.forEach(([optionName, optionDesc]) => {
if (!optionDesc?.type || !optionDesc.scope || (common && optionDesc.scope === 'generator'))
return;
let optionValue;
// Hidden options are test options, which doesn't rely on commander for options parsing.
// We must parse environment variables manually
if (this.options[optionDesc.name ?? optionName] === undefined && optionDesc.env && process.env[optionDesc.env]) {
optionValue = process.env[optionDesc.env];
}
else {
optionValue = this.options[optionDesc.name ?? optionName];
}
if (optionValue !== undefined) {
optionValue = optionDesc.type !== Array && optionDesc.type !== Function ? optionDesc.type(optionValue) : optionValue;
if (optionDesc.scope === 'storage') {
this.config.set(optionName, optionValue);
}
else if (optionDesc.scope === 'blueprint') {
this.blueprintStorage.set(optionName, optionValue);
}
else if (optionDesc.scope === 'control') {
this.sharedData.getControl()[optionName] = optionValue;
}
else if (optionDesc.scope === 'generator') {
this[optionName] = optionValue;
}
else if (optionDesc.scope === 'context') {
this.context[optionName] = optionValue;
}
else if (optionDesc.scope !== 'none') {
throw new Error(`Scope ${optionDesc.scope} not supported`);
}
}
else if (optionDesc.default !== undefined && optionDesc.scope === 'generator' && this[optionName] === undefined) {
this[optionName] = optionDesc.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 {
// Varags 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);
if (argumentDef.scope === undefined || argumentDef.scope === 'generator') {
this[argumentName] = convertedValue;
}
else if (argumentDef.scope === 'context') {
this.context[argumentName] = convertedValue;
}
else if (argumentDef.scope === 'storage') {
this.config.set(argumentName, convertedValue);
}
else if (argumentDef.scope === 'blueprint') {
this.blueprintStorage.set(argumentName, convertedValue);
}
}
}
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;
if ((def.scope ?? 'storage') === 'storage') {
storage = this.config;
if (promptSpec.default === undefined) {
promptSpec = { ...promptSpec, default: () => this.jhipsterConfigWithDefaults?.[name] };
}
}
else if (def.scope === 'blueprint') {
storage = this.blueprintStorage;
}
else if (def.scope === 'generator') {
storage = {
getPath: path => get(this, path),
setPath: (path, value) => set(this, path, value),
};
}
else if (def.scope === 'context') {
storage = {
getPath: path => get(this.context, path),
setPath: (path, value) => set(this.context, path, value),
};
}
return {
name,
choices: def.choices,
...promptSpec,
storage,
};
});
}
/**
* Generate a date to be used by Liquibase changelogs.
*
* @param {Boolean} [reproducible=true] - Set true if the changelog date can be reproducible.
* Set false to create a changelog date incrementing the last one.
* @return {String} Changelog date.
*/
dateFormatForLiquibase(reproducible) {
const control = this.sharedData.getControl();
reproducible = reproducible ?? Boolean(control.reproducible);
// Use started counter or use stored creationTimestamp if creationTimestamp option is passed
const creationTimestamp = this.options.creationTimestamp ? this.config.get('creationTimestamp') : undefined;
let now = new Date();
// Miliseconds is ignored for changelogDate.
now.setMilliseconds(0);
// Run reproducible timestamp when regenerating the project with reproducible option or an specific timestamp.
if (reproducible || creationTimestamp) {
if (control.reproducibleLiquibaseTimestamp) {
// Counter already started.
now = control.reproducibleLiquibaseTimestamp;
}
else {
// Create a new counter
const newCreationTimestamp = creationTimestamp ?? this.config.get('creationTimestamp');
now = newCreationTimestamp ? new Date(newCreationTimestamp) : now;
now.setMilliseconds(0);
}
now.setMinutes(now.getMinutes() + 1);
control.reproducibleLiquibaseTimestamp = now;
// Reproducible build can create future timestamp, save it.
const lastLiquibaseTimestamp = this.jhipsterConfig.lastLiquibaseTimestamp;
if (!lastLiquibaseTimestamp || now.getTime() > lastLiquibaseTimestamp) {
this.config.set('lastLiquibaseTimestamp', now.getTime());
}
}
else {
// Get and store lastLiquibaseTimestamp, a future timestamp can be used
let lastLiquibaseTimestamp = this.jhipsterConfig.lastLiquibaseTimestamp;
if (lastLiquibaseTimestamp) {
lastLiquibaseTimestamp = new Date(lastLiquibaseTimestamp);
if (lastLiquibaseTimestamp >= now) {
now = lastLiquibaseTimestamp;
now.setSeconds(now.getSeconds() + 1);
now.setMilliseconds(0);
}
}
this.jhipsterConfig.lastLiquibaseTimestamp = now.getTime();
}
return formatDateForChangelog(now);
}
/**
* Alternative templatePath that fetches from the blueprinted generator, instead of the blueprint.
*/
jhipsterTemplatePath(...path) {
let existingGenerator;
try {
existingGenerator = this._jhipsterGenerator ?? requireNamespace(this.options.namespace).generator;
}
catch {
if (this.options.namespace) {
const split = this.options.namespace.split(':', 2);
existingGenerator = split.length === 1 ? split[0] : split[1];
}
else {
throw new Error('Could not determine the generator name');
}
}
this._jhipsterGenerator = existingGenerator;
return this._jhipsterGenerator
? this.fetchFromInstalledJHipster(this._jhipsterGenerator, 'templates', ...path)
: this.templatePath(...path);
}
/**
* Compose with a jhipster generator using default jhipster config.
* @return {object} the composed generator
*/
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,
},
});
}
/**
* Compose with a jhipster generator using default jhipster config, but queue it immediately.
*/
async dependsOnJHipster(generator, options) {
return this.composeWithJHipster(generator, {
...options,
schedule: false,
});
}
/**
* 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 gen-jhipster instance installed
*/
fetchFromInstalledJHipster(...path) {
if (path) {
return joinPath(__dirname, '..', ...path);
}
return path;
}
/**
* Utility function to write file.
*
* @param source
* @param destination - destination
* @param data - template data
* @param options - options passed to ejs render
* @param copyOptions
*/
writeFile(source, destination, data = this, options, 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, { root, ...options }, { noGlob: true, ...copyOptions });
}
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');
const { sections, blocks, context = this, templates } = options;
const { rootTemplatesPath, customizeTemplatePath = file => file, transform: methodTransform = [] } = options;
const { _: commonSpec = {} } = sections || {};
const { transform: sectionTransform = [] } = commonSpec;
const startTime = new Date().getMilliseconds();
const { customizeTemplatePaths: contextCustomizeTemplatePaths = [] } = context;
const templateData = this.jhipster7Migration
? createJHipster7Context(this, context, { log: this.jhipster7Migration === 'verbose' ? msg => this.log.info(msg) : () => { } })
: context;
/* Build lookup order first has preference.
* Example
* rootTemplatesPath = ['reactive', 'common']
* jhipsterTemplatesFolders = ['/.../generator-jhispter-blueprint/server/templates', '/.../generator-jhispter/server/templates']
*
* /.../generator-jhispter-blueprint/server/templates/reactive/templatePath
* /.../generator-jhispter-blueprint/server/templates/common/templatePath
* /.../generator-jhispter/server/templates/reactive/templatePath
* /.../generator-jhispter/server/templates/common/templatePath
*/
let rootTemplatesAbsolutePath;
if (!rootTemplatesPath) {
rootTemplatesAbsolutePath = this.jhipsterTemplatesFolders;
}
else if (typeof rootTemplatesPath === 'string' && isAbsolute(rootTemplatesPath)) {
rootTemplatesAbsolutePath = rootTemplatesPath;
}
else {
rootTemplatesAbsolutePath = this.jhipsterTemplatesFolders
.map(templateFolder => [].concat(rootTemplatesPath).map(relativePath => join(templateFolder, relativePath)))
.flat();
}
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 && this.getFeatures().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 = [].concat(rootTemplatesAbsolutePath);
for (const contextCustomizeTemplatePath of contextCustomizeTemplatePaths) {
const file = contextCustomizeTemplatePath.call(this, {
namespace: this.options.namespace,
sourceFile,
resolvedSourceFile: sourceFileFrom,
destinationFile: targetFile,
templatesRoots,
}, context);
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 (context.entityClass) {
if (!context.baseName) {
throw new Error('baseName is required at templates context');
}
const sourceBasename = basename(sourceFileFrom);
const seed = `${context.entityClass}-${sourceBasename}${context.fakerSeed ?? ''}`;
Object.values(this.sharedData.getApplication()?.sharedEntities ?? {}).forEach((entity) => {
entity.resetFakerSeed(seed);
});
// Async calls will make the render method to be scheduled, allowing the faker key to change in the meantime.
useAsync = false;
}
const renderOptions = {
...(options?.renderOptions ?? {}),
// Set root for ejs to lookup for partials.
root: templatesRoots,
// ejs caching cause problem https://github.com/jhipster/gen-jhipster/pull/20757
cache: false,
};
const copyOptions = { noGlob: true };
if (appendEjs) {
sourceFileFrom = `${sourceFileFrom}.ejs`;
}
if (useAsync) {
await this.renderTemplateAsync(sourceFileFrom, targetFile, templateData, renderOptions, copyOptions);
}
else {
this.renderTemplate(sourceFileFrom, targetFile, templateData, renderOptions, 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 parsedBlocks = blocks;
if (sections) {
assert(typeof sections === 'object', 'sections must be an object');
const parsedSections = Object.entries(sections)
.map(([sectionName, sectionBlocks]) => {
if (sectionName.startsWith('_'))
return undefined;
assert(Array.isArray(sectionBlocks), `Section must be an array for ${sectionName}`);
return { sectionName, sectionBlocks };
})
.filter(Boolean);
parsedBlocks = parsedSections
.map(({ sectionName, sectionBlocks }) => {
return sectionBlocks.map((block, blockIdx) => {
const blockSpecPath = `${sectionName}[${blockIdx}]`;
assert(typeof block === 'object', `Block must be an object for ${blockSpecPath}`);
return { blockSpecPath, ...block };
});
})
.flat();
}
let parsedTemplates;
if (parsedBlocks) {
parsedTemplates = parsedBlocks
.map((block, blockIdx) => {
const { blockSpecPath = `${blockIdx}`, path: blockPathValue = './', from: blockFromCallback, to: blockToCallback, condition: blockConditionCallback, transform: blockTransform = [], renameTo: blockRenameTo, } = block;
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, context);
}
let { noEjs } = fileSpec;
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, context, fileSpec, this));
}
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, context, destinationFile, this));
}
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;
}
// TODO remove for jhipster 8
if (noEjs === undefined) {
const { method } = fileSpec;
if (method === 'copy') {
noEjs = true;
}
}
return {
condition,
sourceFile,
destinationFile,
options,
transform: derivedTransform,
noEjs,
binary,
};
});
})
.flat()
.filter(template => template);
}
else {
parsedTemplates = 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(file => file);
}
editFile(file, options, ...transformCallbacks) {
let actualOptions;
if (typeof options === 'function') {
transformCallbacks = [options, ...transformCallbacks];
actualOptions = {};
}
else if (options === undefined) {
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 (!originalContent) {
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) => {
try {
newContent = joinCallbacks(...callbacks).call(this, newContent, filePath);
if (actualOptions.assertModified && originalContent === newContent) {
const errorMessage = `${chalk.yellow('Fail to modify ')}${filePath}.`;
if (!this.ignoreNeedlesError) {
throw new Error(errorMessage);
}
this.log(errorMessage);
}
this.writeDestination(filePath, 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));
}