UNPKG

gen-jhipster

Version:

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

443 lines (442 loc) 17.8 kB
/** * 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 assert from 'assert'; import { existsSync } from 'fs'; import path, { dirname, resolve } from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; import chalk from 'chalk'; import { cloneDeep, mergeWith } from 'lodash-es'; import Environment from 'yeoman-environment'; import { QueuedAdapter } from '@yeoman/adapter'; import { createJHipsterLogger, packageNameToNamespace } from '../generators/base/support/index.js'; import { loadBlueprintsFromConfiguration, mergeBlueprints, parseBlueprintInfo } from '../generators/base/internal/index.js'; import { readCurrentPathYoRcFile } from '../lib/utils/yo-rc.js'; import { CLI_NAME, logger } from './utils.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const jhipsterDevBlueprintPath = process.env.JHIPSTER_DEV_BLUEPRINT === 'true' ? path.join(__dirname, '../.blueprint') : undefined; const devBlueprintNamespace = '@jhipster/jhipster-dev'; const localBlueprintNamespace = '@jhipster/jhipster-local'; const defaultLookupOptions = { lookups: ['generators', 'generators/*/generators'], customizeNamespace: ns => ns?.replaceAll(':generators:', ':').replaceAll('gen-', ''), }; const createEnvironment = (options = {}) => { options.adapter = options.adapter ?? new QueuedAdapter({ log: createJHipsterLogger() }); return new Environment({ newErrorHandler: true, ...options }); }; export default class EnvironmentBuilder { /** @type {Environment} */ env; devBlueprintPath; localBlueprintPath; localBlueprintExists; /** * Creates a new EnvironmentBuilder with a new Environment. * * @param {any} [args] - Arguments passed to Environment.createEnv(). * @param {Object} [options] - options passed to Environment.createEnv(). * @param [adapter] - adapter passed to Environment.createEnv(). * @return {EnvironmentBuilder} envBuilder */ static create(options = {}) { const env = createEnvironment(options); env.setMaxListeners(0); return new EnvironmentBuilder(env); } /** * Creates a new Environment with blueprints. * * Can be used to create a new test environment (requires yeoman-test >= 2.6.0): * @example * const promise = require('yeoman-test').create('jhipster:app', {}, {createEnv: EnvironmentBuilder.createEnv}).run(); * * @param {...any} args - Arguments passed to Environment.createEnv(). * @return {Promise<Environment>} envBuilder */ static async createEnv(...args) { const builder = await EnvironmentBuilder.createDefaultBuilder(...args); return builder.getEnvironment(); } /** * Creates a new EnvironmentBuilder with a new Environment and load jhipster, blueprints and sharedOptions. * * @param {...any} args - Arguments passed to Environment.createEnv(). * @return {EnvironmentBuilder} envBuilder */ static async createDefaultBuilder(...args) { return EnvironmentBuilder.create(...args).prepare(); } static async run(args, generatorOptions = {}, envOptions = {}) { const envBuilder = await EnvironmentBuilder.createDefaultBuilder(envOptions); const env = envBuilder.getEnvironment(); await env.run(args, generatorOptions); } /** * Class to manipulate yeoman environment for jhipster needs. * - Registers jhipster generators. * - Loads blueprints from argv and .yo-rc.json. * - Installs blueprints if not found. * - Loads sharedOptions. */ constructor(env) { this.env = env; } async prepare({ blueprints, lookups, devBlueprintPath = jhipsterDevBlueprintPath } = {}) { this.devBlueprintPath = existsSync(devBlueprintPath) ? devBlueprintPath : undefined; this.localBlueprintPath = path.join(process.cwd(), '.blueprint'); this.localBlueprintExists = this.localBlueprintPath !== this.devBlueprintPath && existsSync(this.localBlueprintPath); await this._lookupJHipster(); await this._lookupLocalBlueprint(); await this._lookupDevBlueprint(); this._loadBlueprints(blueprints); await this._lookups(lookups); await this._lookupBlueprints(); await this._loadSharedOptions(); return this; } getBlueprintsNamespaces() { return [ ...Object.keys(this._blueprintsWithVersion).map(packageName => packageNameToNamespace(packageName)), localBlueprintNamespace, ...(this.devBlueprintPath ? [devBlueprintNamespace] : []), ]; } /** * Construct blueprint option value. * * @return {String} */ getBlueprintsOption() { return Object.entries(this._blueprintsWithVersion) .map(([packageName, packageVersion]) => (packageVersion ? `${packageName}@${packageVersion}` : packageName)) .join(','); } /** * @private * Lookup current jhipster generators. * * @return {EnvironmentBuilder} this for chaining. */ async _lookupJHipster() { // Register jhipster generators. const sourceRoot = path.basename(path.join(__dirname, '..')); let packagePath; let lookup; if (sourceRoot === 'gen-jhipster') { packagePath = path.join(__dirname, '..'); lookup = 'generators'; } else { packagePath = path.join(__dirname, '../..'); lookup = `${sourceRoot}/generators`; } const generators = await this.env.lookup({ ...defaultLookupOptions, packagePaths: [packagePath], lookups: [lookup, `${lookup}/*/generators`], }); generators.forEach(generator => { // Verify jhipster generators namespace. assert(generator.namespace.startsWith(`${CLI_NAME}:`), `Error on the registered namespace ${generator.namespace}, make sure your folder is called gen-jhipster.`); }); return this; } async _lookupLocalBlueprint() { if (this.localBlueprintExists) { // Register jhipster generators. const generators = await this.env.lookup({ packagePaths: [this.localBlueprintPath], lookups: ['.'], customizeNamespace: ns => ns?.replace('.blueprint', '@jhipster/jhipster-local'), }); if (generators.length > 0) { this.env.sharedOptions.composeWithLocalBlueprint = true; } } return this; } async _lookupDevBlueprint() { // Register jhipster generators. await this.env.lookup({ packagePaths: [this.devBlueprintPath], lookups: ['.'], customizeNamespace: ns => ns?.replace('.blueprint', '@jhipster/jhipster-dev'), }); return this; } async _lookups(lookups = []) { lookups = [].concat(lookups); for (const lookup of lookups) { await this.env.lookup({ ...defaultLookupOptions, ...lookup }); } return this; } /** * @private * Load blueprints from argv, .yo-rc.json. * * @return {EnvironmentBuilder} this for chaining. */ _loadBlueprints(blueprints) { this._blueprintsWithVersion = { ...this._getAllBlueprintsWithVersion(), ...blueprints, }; return this; } /** * @private * Lookup current loaded blueprints. * * @param {Object} [options] - forwarded to Environment lookup. * @return {EnvironmentBuilder} this for chaining. */ async _lookupBlueprints(options) { const missingBlueprints = Object.keys(this._blueprintsWithVersion).filter(blueprint => !this.env.isPackageRegistered(packageNameToNamespace(blueprint))); if (missingBlueprints && missingBlueprints.length > 0) { // Lookup for blueprints. await this.env.lookup({ ...defaultLookupOptions, ...options, filterPaths: true, packagePatterns: missingBlueprints, }); } return this; } /** * Lookup for generators. * * @param {string[]} [generators] - generators to lookup. * @param {Object} [options] - forwarded to Environment lookup. * @return {EnvironmentBuilder} this for chaining. */ async lookupGenerators(generators, options = {}) { await this.env.lookup({ filterPaths: true, ...options, packagePatterns: generators }); return this; } /** * @private * Load sharedOptions from jhipster and blueprints. * * @return {Promise<EnvironmentBuilder>} this for chaining. */ async _loadSharedOptions() { const blueprintsPackagePath = await this._getBlueprintPackagePaths(); if (blueprintsPackagePath) { const sharedOptions = (await this._getSharedOptions(blueprintsPackagePath)) ?? {}; // Env will forward sharedOptions to every generator Object.assign(this.env.sharedOptions, sharedOptions); } return this; } /** * Get blueprints commands. * * @return {Record<string, import('./types.js').CliCommand>} blueprint commands. */ async getBlueprintCommands() { let blueprintsPackagePath = await this._getBlueprintPackagePaths(); if (this.devBlueprintPath) { blueprintsPackagePath = blueprintsPackagePath ?? []; blueprintsPackagePath.push([devBlueprintNamespace, this.devBlueprintPath]); if (this.localBlueprintExists) { blueprintsPackagePath.push([localBlueprintNamespace, this.localBlueprintPath]); } } return this._getBlueprintCommands(blueprintsPackagePath); } /** * Get the environment. * * @return {Environment} the yeoman environment. */ getEnvironment() { return this.env; } /** * @private * Load blueprints from argv. * At this point, commander has not parsed yet because we are building it. * @returns {Blueprint[]} */ _getBlueprintsFromArgv() { const blueprintNames = []; const indexOfBlueprintArgv = process.argv.indexOf('--blueprint'); if (indexOfBlueprintArgv > -1) { blueprintNames.push(process.argv[indexOfBlueprintArgv + 1]); } const indexOfBlueprintsArgv = process.argv.indexOf('--blueprints'); if (indexOfBlueprintsArgv > -1) { blueprintNames.push(...process.argv[indexOfBlueprintsArgv + 1].split(',')); } if (!blueprintNames.length) { return []; } return blueprintNames.map(v => parseBlueprintInfo(v)); } /** * @private * Load blueprints from .yo-rc.json. * @returns {Blueprint[]} */ _getBlueprintsFromYoRc() { const yoRc = readCurrentPathYoRcFile(); if (!yoRc?.['gen-jhipster']) { return []; } return loadBlueprintsFromConfiguration(yoRc['gen-jhipster']); } /** * @private * Creates a 'blueprintName: blueprintVersion' object from argv and .yo-rc.json blueprints. */ _getAllBlueprintsWithVersion() { return mergeBlueprints(this._getBlueprintsFromArgv(), this._getBlueprintsFromYoRc()).reduce((acc, blueprint) => { acc[blueprint.name] = blueprint.version; return acc; }, {}); } /** * @private * Get packagePaths from current loaded blueprints. */ async _getBlueprintPackagePaths() { const blueprints = this._blueprintsWithVersion; if (!blueprints || Object.keys(blueprints).length === 0) { return undefined; } const blueprintsToInstall = Object.entries(blueprints) .filter(([blueprint, _version]) => { const namespace = packageNameToNamespace(blueprint); if (!this.env.getPackagePath(namespace)) { this.env.lookupLocalPackages(blueprint); } return !this.env.getPackagePath(namespace); }) .reduce((acc, [blueprint, version]) => { acc[blueprint] = version; return acc; }, {}); if (Object.keys(blueprintsToInstall).length > 0) { await this.env.installLocalGenerators(blueprintsToInstall); } return Object.entries(blueprints).map(([blueprint, _version]) => { const namespace = packageNameToNamespace(blueprint); const packagePath = this.env.getPackagePath(namespace); if (!packagePath) { logger.fatal(`The ${chalk.yellow(blueprint)} blueprint provided is not installed. Please install it using command ${chalk.yellow(`npm i -g ${blueprint}`)}`); } return [blueprint, packagePath]; }); } /** * @private * Get blueprints commands. * * @return {Record<string, import('./types.js').CliCommand>} commands. */ async _getBlueprintCommands(blueprintPackagePaths) { if (!blueprintPackagePaths) { return undefined; } let result; for (const [blueprint, packagePath] of blueprintPackagePaths) { let blueprintCommand; const blueprintCommandFile = `${packagePath}/cli/commands`; const blueprintCommandExtension = ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts'].find(extension => existsSync(`${blueprintCommandFile}${extension}`)); if (blueprintCommandExtension) { const blueprintCommandsUrl = pathToFileURL(resolve(`${blueprintCommandFile}${blueprintCommandExtension}`)); try { blueprintCommand = (await import(blueprintCommandsUrl)).default; const blueprintCommands = cloneDeep(blueprintCommand); Object.entries(blueprintCommands).forEach(([_command, commandSpec]) => { commandSpec.blueprint = commandSpec.blueprint || blueprint; }); result = { ...result, ...blueprintCommands }; } catch { const msg = `Error parsing custom commands found within blueprint: ${blueprint} at ${blueprintCommandsUrl}`; /* eslint-disable no-console */ console.info(`${chalk.green.bold('INFO!')} ${msg}`); } } else { const msg = `No custom commands found within blueprint: ${blueprint} at ${packagePath}`; /* eslint-disable no-console */ console.info(`${chalk.green.bold('INFO!')} ${msg}`); } } return result; } /** * @private * Get blueprints sharedOptions. * * @return {Object} sharedOptions. */ async _getSharedOptions(blueprintPackagePaths) { function joiner(objValue, srcValue) { if (objValue === undefined) { return srcValue; } if (Array.isArray(objValue) && Array.isArray(srcValue)) { return objValue.concat(srcValue); } if (Array.isArray(objValue)) { return [...objValue, srcValue]; } if (Array.isArray(srcValue)) { return [objValue, ...srcValue]; } return [objValue, srcValue]; } async function loadSharedOptionsFromFile(sharedOptionsBase, msg, errorMsg) { try { const baseExtension = ['.js', '.cjs', '.mjs'].find(extension => existsSync(resolve(`${sharedOptionsBase}${extension}`))); if (baseExtension) { const { default: opts } = await import(pathToFileURL(resolve(`${sharedOptionsBase}${baseExtension}`))); /* eslint-disable no-console */ if (msg) { console.info(`${chalk.green.bold('INFO!')} ${msg}`); } return opts; } } catch (e) { if (errorMsg) { console.info(`${chalk.green.bold('INFO!')} ${errorMsg}`, e); } } return {}; } const localPath = './.jhipster/sharedOptions'; let result = await loadSharedOptionsFromFile(localPath, `SharedOptions found at local config ${localPath}`); if (!blueprintPackagePaths) { return undefined; } for (const [blueprint, packagePath] of blueprintPackagePaths) { const errorMsg = `No custom sharedOptions found within blueprint: ${blueprint} at ${packagePath}`; const opts = await loadSharedOptionsFromFile(`${packagePath}/cli/sharedOptions`, undefined, errorMsg); result = mergeWith(result, opts, joiner); } return result; } }