diff --git a/cli/program.mts b/cli/program.mts index 33ca7800e992..cf90ecc7efc3 100644 --- a/cli/program.mts +++ b/cli/program.mts @@ -152,7 +152,10 @@ const addCommandGeneratorOptions = async ( } } try { - if (root || !generatorModule.command || generatorModule.command.loadGeneratorOptions) { + if ( + generatorModule.command?.loadGeneratorOptions !== false && + (root || !generatorModule.command || generatorModule.command.loadGeneratorOptions) + ) { const generator = await generatorMeta.instantiateHelp(); // Add basic yeoman generator options command.addGeneratorOptions(generator._options, blueprintOptionDescription); diff --git a/lib/command/generator-command.spec.ts b/lib/command/generator-command.spec.ts new file mode 100644 index 000000000000..95b14e741947 --- /dev/null +++ b/lib/command/generator-command.spec.ts @@ -0,0 +1,294 @@ +import { describe, expect } from 'esmocha'; +import type { GeneratorMeta } from '@yeoman/types'; +import { defaultHelpers as helpers, runResult } from '../testing/index.js'; +import BaseApplicationGenerator from '../../generators/base-application/generator.js'; +import type { JHipsterCommandDefinition, JHipsterConfig } from './types.js'; + +const notImplementedCallback = (methodName: string) => { + return () => { + throw new Error(`${methodName} not implemented`); + }; +}; + +const dummyMeta = { + packageNamespace: 'jhipster', + resolved: 'dummy', + importModule: () => Promise.resolve({ command: { loadGeneratorOptions: false } }), + importGenerator: notImplementedCallback('importGenerator'), + instantiateHelp: notImplementedCallback('instantiateHelp'), + instantiate: notImplementedCallback('instantiate'), +}; + +class CommandGenerator extends BaseApplicationGenerator { + context = {}; + + constructor(args, opts, features) { + super(args, opts, { ...features, queueCommandTasks: true, jhipsterBootstrap: false }); + this.customLifecycle = true; + } +} + +const runDummyCli = (cliArgs: string, config: JHipsterConfig) => { + return helpers + .runCli(cliArgs.startsWith('jdl ') ? cliArgs : `dummy ${cliArgs}`.trim(), { + useEnvironmentBuilder: false, + entrypointGenerator: 'dummy', + commands: { + dummy: { desc: 'dummy Generator' }, + }, + }) + .withJHipsterConfig() + .onEnvironment(env => { + if (!config) { + throw new Error('command not set'); + } + + const metaStore: Record = (env as any).store._meta; + metaStore['jhipster:dummy'] = { + ...dummyMeta, + namespace: 'jhipster:dummy', + importModule: () => + Promise.resolve({ + command: { configs: { testOption: config }, loadGeneratorOptions: false } satisfies JHipsterCommandDefinition, + }), + importGenerator: () => Promise.resolve(CommandGenerator as any), + }; + metaStore['jhipster:bootstrap'] = { + ...dummyMeta, + namespace: 'jhipster:bootstrap', + }; + }); +}; + +const expectGeneratorOptionsTestOption = () => expect((runResult.generator.options as any).testOption); +const expectGeneratorTestOption = () => expect((runResult.generator as any).testOption); +const expectContextTestOption = () => expect(runResult.generator.context!.testOption); +const expectJHipsterConfigTestOption = () => expect(runResult.generator.jhipsterConfig.testOption); +const expectBlueprintConfigTestOption = () => expect((runResult.generator as any).blueprintConfig.testOption); +const expectApplicationTestOption = () => expect(runResult.generator.sharedData.getApplication().testOption); + +describe('generator commands', () => { + for (const scope of ['generator', 'context', 'storage', 'blueprint', 'none'] as const) { + describe(`${scope} scoped`, () => { + const checkOptions = (value: any, argument = false) => { + if (argument) { + // Argument is passed through positionalArguments option. + expectGeneratorOptionsTestOption().toBeUndefined(); + } else if (typeof value === 'number') { + // Option value is not converted to number yet. + expectGeneratorOptionsTestOption().toEqual(String(value)); + } else if (Array.isArray(value)) { + expectGeneratorOptionsTestOption().toEqual(value); + } else { + expectGeneratorOptionsTestOption().toBe(value); + } + + if (scope !== 'generator') { + expectGeneratorTestOption().toBeUndefined(); + } else if (Array.isArray(value)) { + expectGeneratorTestOption().toEqual(value); + } else { + expectGeneratorTestOption().toBe(value); + } + + if (scope !== 'context') { + expectContextTestOption().toBeUndefined(); + } else if (Array.isArray(value)) { + expectContextTestOption().toEqual(value); + } else { + expectContextTestOption().toBe(value); + } + + if (scope !== 'blueprint') { + expectBlueprintConfigTestOption().toBeUndefined(); + } else if (Array.isArray(value)) { + expectBlueprintConfigTestOption().toEqual(value); + } else { + expectBlueprintConfigTestOption().toBe(value); + } + + if (!['application', 'storage', 'blueprint'].includes(scope)) { + expectApplicationTestOption().toBeUndefined(); + } else if (Array.isArray(value)) { + expectApplicationTestOption().toEqual(value); + } else { + expectApplicationTestOption().toBe(value); + } + + // Storage scope is same as application scope with storage. + if (scope !== 'storage') { + expectJHipsterConfigTestOption().toBeUndefined(); + } else if (Array.isArray(value)) { + expectJHipsterConfigTestOption().toEqual(value); + } else { + expectJHipsterConfigTestOption().toBe(value); + } + }; + + describe('cli option', () => { + describe('boolean', () => { + const config: JHipsterConfig = { + cli: { + type: Boolean, + }, + scope, + }; + + it('without options', async () => { + await runDummyCli('', config); + checkOptions(undefined); + }); + it('with true option', async () => { + await runDummyCli('--test-option', config); + checkOptions(true); + }); + it('with false option', async () => { + await runDummyCli('--no-test-option', config); + checkOptions(false); + }); + }); + + describe('string', () => { + const config: JHipsterConfig = { + cli: { + type: String, + }, + scope, + }; + + it('without options', async () => { + await runDummyCli('', config); + checkOptions(undefined); + }); + it('with option value', async () => { + await runDummyCli('--test-option 1', config); + checkOptions('1'); + }); + }); + + describe('number', () => { + const config: JHipsterConfig = { + cli: { + type: Number, + }, + scope, + }; + + it('without options', async () => { + await runDummyCli('', config); + checkOptions(undefined); + }); + it('with option value', async () => { + await runDummyCli('--test-option 1', config); + checkOptions(1); + }); + }); + + describe('array', () => { + const config: JHipsterConfig = { + cli: { + type: Array, + }, + scope, + }; + + it('without options', async () => { + await runDummyCli('', config); + checkOptions(undefined); + }); + it('with option value', async () => { + await runDummyCli('--test-option 1', config); + checkOptions(['1']); + }); + it('with option values', async () => { + await runDummyCli('--test-option 1 2', config); + checkOptions(['1', '2']); + }); + }); + }); + describe('cli argument', () => { + describe('string', () => { + const config: JHipsterConfig = { + argument: { + type: String, + }, + scope, + }; + + it('without argument', async () => { + await runDummyCli('', config); + checkOptions(undefined, true); + }); + it('with argument value', async () => { + await runDummyCli('1', config); + checkOptions('1', true); + }); + }); + + describe('array', () => { + const config: JHipsterConfig = { + argument: { + type: Array, + }, + scope, + }; + + it('without arguments', async () => { + await runDummyCli('', config); + checkOptions(undefined, true); + }); + it('with argument value', async () => { + await runDummyCli('1', config); + checkOptions(['1'], true); + }); + it('with arguments values', async () => { + await runDummyCli('1 2', config); + checkOptions(['1', '2'], true); + }); + }); + }); + + describe.skip('prompt', () => { + describe('input', () => { + const config: JHipsterConfig = { + prompt: { + message: 'testOption', + type: 'input', + }, + scope, + }; + + it('with option', async () => { + await runDummyCli('', config).withAnswers({ testOption: '1' }); + checkOptions('1'); + }); + }); + }); + + describe.skip('jdl', () => { + describe('boolean jdl option', () => { + const config: JHipsterConfig = { + jdl: { + type: 'boolean', + tokenType: 'BOOLEAN', + }, + scope, + }; + + it('without options', async () => { + await runDummyCli('jdl --inline ""', config); + checkOptions(undefined); + }); + it('with true option', async () => { + await runDummyCli('--test-option', config); + checkOptions(true); + }); + it('with false option', async () => { + await runDummyCli('--no-test-option', config); + checkOptions(false); + }); + }); + }); + }); + } +}); diff --git a/lib/command/types.d.ts b/lib/command/types.d.ts index 77950a956f90..d5be589db037 100644 --- a/lib/command/types.d.ts +++ b/lib/command/types.d.ts @@ -31,7 +31,7 @@ export type PromptSpec = { type JHipsterArgumentConfig = SetOptional & { scope?: CommandConfigScope }; -type CliSpec = SetOptional & { +type CliSpec = Omit, 'storage'> & { env?: string; /** * Imply other options. @@ -48,9 +48,6 @@ export type ConfigSpec = { | PromptSpec | ((gen: ConfigContext & { jhipsterConfigWithDefaults: Record }, config: ConfigSpec) => PromptSpec); readonly jdl?: Omit; - readonly storage?: { - readonly type?: typeof Boolean | typeof String | typeof Number | typeof Array; - }; readonly scope?: CommandConfigScope; /** * The callback receives the generator as input for 'generator' scope. @@ -76,7 +73,7 @@ export type JHipsterArguments = Record; export type JHipsterOptions = Record; -export type JHipsterConfig = RequireAtLeastOne, 'argument' | 'cli' | 'prompt' | 'storage'>; +export type JHipsterConfig = RequireAtLeastOne, 'argument' | 'cli' | 'prompt' | 'jdl'>; export type JHipsterConfigs = Record>; diff --git a/lib/testing/helpers.spec.ts b/lib/testing/helpers.spec.ts index d51c8a086a6f..76ceecd9a4e4 100644 --- a/lib/testing/helpers.spec.ts +++ b/lib/testing/helpers.spec.ts @@ -10,7 +10,7 @@ describe('helpers', () => { }); it('should register not jhipster generators namespaces', () => { expect( - Object.keys(runResult.env.store._meta) + Object.keys((runResult.env as any).store._meta) .filter(ns => ns !== DUMMY_NAMESPACE) .sort(), ).toHaveLength(0); @@ -22,7 +22,7 @@ describe('helpers', () => { }); it('should register jhipster generators namespaces', () => { expect( - Object.keys(runResult.env.store._meta) + Object.keys((runResult.env as any).store._meta) .filter(ns => ns !== DUMMY_NAMESPACE) .sort(), ).toMatchSnapshot(); @@ -34,7 +34,7 @@ describe('helpers', () => { }); it('should register jhipster generators namespaces', () => { expect( - Object.keys(runResult.env.store._meta) + Object.keys((runResult.env as any).store._meta) .filter(ns => ns !== DUMMY_NAMESPACE) .sort(), ).toMatchSnapshot(); @@ -48,7 +48,7 @@ describe('helpers', () => { }); it('should register jhipster generators namespaces', () => { expect( - Object.keys(runResult.env.store._meta) + Object.keys((runResult.env as any).store._meta) .filter(ns => ns !== DUMMY_NAMESPACE) .sort(), ).toMatchSnapshot(); diff --git a/lib/testing/helpers.ts b/lib/testing/helpers.ts index 9185eff065e8..3800ef52c204 100644 --- a/lib/testing/helpers.ts +++ b/lib/testing/helpers.ts @@ -3,6 +3,7 @@ import { mock } from 'node:test'; import { merge, set, snakeCase } from 'lodash-es'; import type { RunContextSettings, RunResult } from 'yeoman-test'; import { RunContext, YeomanTest, result } from 'yeoman-test'; +import type Environment from 'yeoman-environment'; import { globSync } from 'glob'; import type { BaseEnvironmentOptions, GetGeneratorConstructor, BaseGenerator as YeomanGenerator } from '@yeoman/types'; @@ -19,6 +20,7 @@ import type { ApplicationConfiguration } from '../types/application/yo-rc.js'; import { getDefaultJDLApplicationConfig } from '../command/jdl.js'; import type { Entity } from '../types/base/entity.js'; import { buildJHipster, createProgram } from '../../cli/program.mjs'; +import type { CliCommand } from '../../cli/types.js'; import getGenerator, { getGeneratorRelativeFolder } from './get-generator.js'; type GeneratorTestType = YeomanGenerator; @@ -36,6 +38,9 @@ type WithJHipsterGenerators = { * Filter to mock a generator. */ useMock?: (ns: string) => boolean; +}; + +type RunJHipster = WithJHipsterGenerators & { /** * Use the EnviromentBuilder default preparation to create the environment. * Includes local and dev blueprints. @@ -43,7 +48,9 @@ type WithJHipsterGenerators = { useEnvironmentBuilder?: boolean; }; -type JHipsterRunResult = RunResult & { +type JHipsterRunResult = Omit, 'env'> & { + env: Environment; + /** * First argument of mocked source calls. */ @@ -446,36 +453,51 @@ class JHipsterTest extends YeomanTest { settings?: RunContextSettings | undefined, envOptions?: BaseEnvironmentOptions | undefined, ): JHipsterRunContext; - runJHipster(jhipsterGenerator: string, options?: WithJHipsterGenerators): JHipsterRunContext; + runJHipster(jhipsterGenerator: string, options?: RunJHipster): JHipsterRunContext; runJHipster( jhipsterGenerator: string, - settings?: RunContextSettings | WithJHipsterGenerators | undefined, + settings: RunContextSettings | RunJHipster | undefined, envOptions?: BaseEnvironmentOptions | undefined, ): JHipsterRunContext { if (!isAbsolute(jhipsterGenerator)) { jhipsterGenerator = toJHipsterNamespace(jhipsterGenerator); } - const isWithJHipsterGenerators = (opt: any): opt is WithJHipsterGenerators | undefined => + const isRunJHipster = (opt: any): opt is RunJHipster | undefined => opt === undefined || 'actualGeneratorsList' in opt || 'useMock' in opt || 'useDefaultMocks' in opt || 'useEnvironmentBuilder' in opt; - if (isWithJHipsterGenerators(settings)) { - const createEnv = settings?.useEnvironmentBuilder ? createEnvBuilderEnvironment : undefined; - return this.run(jhipsterGenerator, undefined, { createEnv }).withJHipsterGenerators(settings); + if (isRunJHipster(settings)) { + const { useEnvironmentBuilder, ...otherOptions } = settings ?? {}; + if (useEnvironmentBuilder) { + return this.run(jhipsterGenerator, undefined, { createEnv: createEnvBuilderEnvironment }); + } + // If not using EnvironmentBuilder, use the default JHipster generators lookup. + return this.run(jhipsterGenerator).withJHipsterGenerators(otherOptions); } return this.run(getGenerator(jhipsterGenerator), settings, envOptions).withJHipsterGenerators(); } - runCli(command: string | string[]): JHipsterRunContext { + runCli( + command: string | string[], + options: { commands?: Record; useEnvironmentBuilder?: boolean; entrypointGenerator?: string } = {}, + ): JHipsterRunContext { + const { useEnvironmentBuilder, ...buildJHipsterOptions } = options; // Use a dummy generator which will not be used to match yeoman-test requirement. - return this.run(this.createDummyGenerator(), { namespace: 'non-used-dummy:generator' }) - .withJHipsterGenerators({ useEnvironmentBuilder: true }) - .withEnvironmentRun(async function (this, env) { - // Customize program to throw an error instead of exiting the process on cli parse error. - const program = createProgram().exitOverride(); - await buildJHipster({ program, env: env as any, silent: true }); - await program.parseAsync(['jhipster', 'jhipster', ...(Array.isArray(command) ? command : command.split(' '))]); - // Put the rootGenerator in context to be used in result assertions. - this.generator = env.rootGenerator(); - }); + const context = this.run( + this.createDummyGenerator(), + { namespace: 'non-used-dummy:generator' }, + useEnvironmentBuilder ? { createEnv: createEnvBuilderEnvironment } : undefined, + ); + if (!useEnvironmentBuilder) { + // If not using EnvironmentBuilder, use the default JHipster generators lookup. + context.withJHipsterGenerators(); + } + return context.withEnvironmentRun(async function (this, env) { + // Customize program to throw an error instead of exiting the process on cli parse error. + const program = createProgram().exitOverride(); + await buildJHipster({ program, env: env as any, silent: true, ...buildJHipsterOptions }); + await program.parseAsync(['jhipster', 'jhipster', ...(Array.isArray(command) ? command : command.split(' '))]); + // Put the rootGenerator in context to be used in result assertions. + this.generator = env.rootGenerator(); + }); } /**