gen-jhipster
Version:
VHipster - Spring Boot + Angular/React/Vue in one handy generator
247 lines (246 loc) • 10.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 { transformContents } from '@yeoman/transform';
import { escapeRegExp, kebabCase } from 'lodash-es';
import { joinCallbacks } from "./write-files.js";
const needlesWhiteList = [
'liquibase-add-incremental-changelog', // mandatory for incremental changelogs
];
/**
* Change spaces sequences and characters that prettier breaks line (<>()) to allow any number of spaces or new line prefix
*/
export const convertToPrettierExpressions = (str) => str
.replace(/(<|\\\()(?! )/g, String.raw `$1\n?[\s]*`)
.replace(/(?! )(>|\\\))/g, String.raw `,?\n?[\s]*$1`)
.replace(/\s+/g, String.raw `[\s\n]*`);
const isArrayOfContentToAdd = (value) => {
return Array.isArray(value) && value.every(item => typeof item === 'object' && 'content' in item);
};
export const createNeedleRegexp = (needle) => new RegExp(String.raw `(?://|<!--|\{?/\*|#) ${needle}(?: [^$\n]*)?(?:$|\n)`, 'g');
export const getNeedlesPositions = (content, needle = String.raw `jhipster-needle-(?:[-\w]*)`) => {
const regexp = createNeedleRegexp(needle);
const positions = [];
let match;
while ((match = regexp.exec(content))) {
if (needlesWhiteList.some(whileList => match[0].includes(whileList))) {
continue;
}
const needleLineIndex = content.lastIndexOf('\n', match.index) + 1;
positions.unshift({ start: needleLineIndex, end: regexp.lastIndex });
}
return positions;
};
/**
* Check if contentToCheck existing in content
*
* @param contentToCheck
* @param content
* @param [ignoreWhitespaces=true]
*/
export const checkContentIn = (contentToCheck, content, ignoreWhitespaces = true) => {
assert(content, 'content is required');
assert(contentToCheck, 'contentToCheck is required');
let re;
if (typeof contentToCheck === 'string') {
const pattern = ignoreWhitespaces
? convertToPrettierExpressions(escapeRegExp(contentToCheck))
: contentToCheck
.split('\n')
.map(line => String.raw `\s*${escapeRegExp(line)}`)
.join('\n');
re = new RegExp(pattern);
}
else {
re = contentToCheck;
}
return re.test(content);
};
const addNeedlePrefix = (needle) => {
return needle.includes('jhipster-needle-') ? needle : `jhipster-needle-${needle}`;
};
const hasNeedleStart = (content, needle) => {
const regexpStart = new RegExp(`(?://|<!--|\\{?/\\*|#) ${addNeedlePrefix(needle)}-start(?:.*)\n`, 'g');
const startMatch = regexpStart.exec(content);
return Boolean(startMatch);
};
/**
* Write content before needle applying indentation
*
* @param args
* @returns null if needle was not found, new content otherwise
*/
export const insertContentBeforeNeedle = ({ content, contentToAdd, needle, autoIndent = true }) => {
assert(needle, 'needle is required');
assert(content, 'content is required');
assert(contentToAdd, 'contentToAdd is required');
needle = addNeedlePrefix(needle);
const regexp = createNeedleRegexp(needle);
let firstMatch = regexp.exec(content);
if (!firstMatch) {
return null;
}
// Replacements using functions allows to replace multiples needles
if (typeof contentToAdd !== 'function' && regexp.test(content)) {
throw new Error(`Multiple needles found for ${needle}`);
}
const regexpStart = new RegExp(`(?://|<!--|\\{?/\\*|#) ${needle}-start(?:.*)\n`, 'g');
const startMatch = regexpStart.exec(content);
if (startMatch) {
const needleLineIndex = content.lastIndexOf('\n', firstMatch.index) + 1;
content = content.slice(0, startMatch.index + startMatch[0].length) + content.slice(needleLineIndex);
regexp.lastIndex = 0;
firstMatch = regexp.exec(content);
if (!firstMatch) {
throw new Error(`Needle start found for ${needle} but no end found`);
}
}
const needleIndex = firstMatch.index;
const needleLineIndex = content.lastIndexOf('\n', needleIndex) + 1;
const beforeContent = content.slice(0, needleLineIndex);
const afterContent = content.slice(needleLineIndex);
const needleIndent = needleIndex - needleLineIndex;
if (typeof contentToAdd === 'function') {
const newContent = contentToAdd(content, {
needleIndex,
needleLineIndex,
needleIndent,
indentPrefix: ' '.repeat(needleIndent),
});
return newContent;
}
contentToAdd = Array.isArray(contentToAdd) ? contentToAdd : [contentToAdd];
if (autoIndent) {
contentToAdd = contentToAdd.flatMap(eachContentToAdd => eachContentToAdd.split('\n'));
}
// Normalize needle indent with contentToAdd.
const firstContent = contentToAdd.find(line => line.trim());
if (!firstContent) {
// File is blank.
return null;
}
const contentIndent = firstContent.length - firstContent.trimStart().length;
if (needleIndent > contentIndent) {
const identToApply = ' '.repeat(needleIndent - contentIndent);
contentToAdd = contentToAdd.map(line => (line ? identToApply + line : line));
}
else if (needleIndent < contentIndent) {
let identToRemove = contentIndent - needleIndent;
contentToAdd
.filter(line => line.trimStart())
.forEach(line => {
const trimmedLine = line.trimStart();
const lineIndent = line.length - trimmedLine.length;
if (lineIndent < identToRemove) {
identToRemove = lineIndent;
}
});
contentToAdd = contentToAdd.map(line => (line.length > identToRemove ? line.slice(identToRemove) : ''));
}
const newContent = `${beforeContent}${contentToAdd.join('\n')}\n${afterContent}`;
return newContent;
};
/**
* Create an callback to insert the new content into existing content.
*
* A `contentToAdd` of string type will remove leading `\n`.
* Leading `\n` allows a prettier template formatting.
*
* @param options
*/
export const createNeedleCallback = ({ needle, contentToAdd, contentToCheck, optional = false, ignoreWhitespaces = true, autoIndent, }) => {
assert(needle !== undefined, 'needle is required');
assert(needle || typeof contentToAdd === 'string', 'end of file needle requires string contentToAdd');
assert(contentToAdd, 'contentToAdd is required');
return function (content, filePath) {
if (isArrayOfContentToAdd(contentToAdd)) {
contentToAdd = contentToAdd.filter(({ content: itemContent, contentToCheck }) => {
return !checkContentIn(contentToCheck ?? itemContent, content, ignoreWhitespaces);
});
if (contentToAdd.length === 0) {
return content;
}
contentToAdd = contentToAdd.map(({ content }) => content);
}
if (contentToCheck && checkContentIn(contentToCheck, content, ignoreWhitespaces)) {
return content;
}
if (typeof contentToAdd !== 'function') {
if (typeof contentToAdd === 'string' && contentToAdd.startsWith('\n')) {
contentToAdd = contentToAdd.slice(1);
}
contentToAdd = Array.isArray(contentToAdd) ? contentToAdd : [contentToAdd];
if (!needle || !hasNeedleStart(content, needle)) {
contentToAdd = contentToAdd.filter(eachContent => !checkContentIn(eachContent, content, ignoreWhitespaces));
}
if (contentToAdd.length === 0) {
return content;
}
}
if (needle === '') {
return `${content}\n${contentToAdd.join('\n')}\n`;
}
const newContent = insertContentBeforeNeedle({
needle,
content,
contentToAdd,
autoIndent,
});
if (newContent) {
return newContent;
}
const message = `Missing ${optional ? 'optional' : 'required'} jhipster-needle ${needle} not found at '${filePath}'`;
if (optional && this) {
this.log.warn(message);
return content;
}
throw new Error(message);
};
};
export function createBaseNeedle(options, needles) {
const actualNeedles = (needles ??= options);
const actualOptions = needles === undefined ? {} : options;
assert(actualNeedles, 'needles is required');
const { needlesPrefix, filePath, ...needleOptions } = actualOptions;
needleOptions.optional = needleOptions.optional ?? false;
needleOptions.ignoreWhitespaces = needleOptions.ignoreWhitespaces ?? true;
const callbacks = Object.entries(actualNeedles)
.filter(([_key, contentToAdd]) => contentToAdd)
.map(([key, contentToAdd]) => createNeedleCallback({ ...needleOptions, needle: `${needlesPrefix ? `${needlesPrefix}-` : ''}${kebabCase(key)}`, contentToAdd }));
assert(callbacks.length > 0, 'At least 1 needle is required');
const callback = callbacks.length === 1 ? callbacks[0] : joinCallbacks(...callbacks);
if (filePath) {
assert(this?.editFile, 'when passing filePath, the generator is required');
return this.editFile(filePath, callback);
}
return callback;
}
export const createNeedleTransform = () => transformContents(content => {
if (content) {
let contentAsString = content.toString();
const positions = getNeedlesPositions(contentAsString);
if (positions.length > 0) {
for (const position of positions) {
contentAsString = contentAsString.slice(0, position.start) + contentAsString.slice(position.end);
}
return Buffer.from(contentAsString);
}
}
return content;
});