diff --git a/actions/abstract.action.ts b/actions/abstract.action.ts index 70355adf8..5acdcefa9 100644 --- a/actions/abstract.action.ts +++ b/actions/abstract.action.ts @@ -1,9 +1,9 @@ -import { Input } from '../commands'; +import { CommandContext } from '../commands'; export abstract class AbstractAction { public abstract handle( - inputs?: Input[], - options?: Input[], + inputs?: CommandContext, + options?: CommandContext, extraFlags?: string[], ): Promise; } diff --git a/actions/add.action.ts b/actions/add.action.ts index 5c5d1d09e..0a87c8472 100644 --- a/actions/add.action.ts +++ b/actions/add.action.ts @@ -1,5 +1,5 @@ import * as chalk from 'chalk'; -import { Input } from '../commands'; +import { CommandContext, CommandContextEntry } from '../commands'; import { getValueOrDefault } from '../lib/compiler/helpers/get-value-or-default'; import { AbstractPackageManager, @@ -23,7 +23,11 @@ import { AbstractAction } from './abstract.action'; const schematicName = 'nest-add'; export class AddAction extends AbstractAction { - public async handle(inputs: Input[], options: Input[], extraFlags: string[]) { + public async handle( + inputs: CommandContext, + options: CommandContext, + extraFlags: string[], + ) { const libraryName = this.getLibraryName(inputs); const packageName = this.getPackageName(libraryName); const collectionName = this.getCollectionName(libraryName, packageName); @@ -32,10 +36,13 @@ export class AddAction extends AbstractAction { const packageInstallSuccess = skipInstall || (await this.installPackage(collectionName, tagName)); if (packageInstallSuccess) { - const sourceRootOption: Input = await this.getSourceRoot( - inputs.concat(options), - ); - options.push(sourceRootOption); + const sourceRootOption: CommandContextEntry = await this.getSourceRoot([ + inputs, + options, + ]); + if (sourceRootOption) { + options.add(sourceRootOption); + } await this.addLibrary(collectionName, options, extraFlags); } else { @@ -50,12 +57,20 @@ export class AddAction extends AbstractAction { } } - private async getSourceRoot(inputs: Input[]): Promise { + private async getSourceRoot( + storages: CommandContext[], + ): Promise { const configuration = await loadConfiguration(); const configurationProjects = configuration.projects; - const appName = inputs.find((option) => option.name === 'project')! - .value as string; + let appName: string | undefined; + for (const storage of storages) { + const maybeProject = storage.get('project'); + if (maybeProject) { + appName = maybeProject.value; + break; + } + } let sourceRoot = appName ? getValueOrDefault(configuration, 'sourceRoot', appName) @@ -117,7 +132,7 @@ export class AddAction extends AbstractAction { private async addLibrary( collectionName: string, - options: Input[], + options: CommandContext, extraFlags: string[], ) { console.info(MESSAGES.LIBRARY_INSTALLATION_STARTS); @@ -125,7 +140,7 @@ export class AddAction extends AbstractAction { schematicOptions.push( new SchematicOption( 'sourceRoot', - options.find((option) => option.name === 'sourceRoot')!.value as string, + options.get('sourceRoot', true).value, ), ); const extraFlagsString = extraFlags ? extraFlags.join(' ') : undefined; @@ -146,15 +161,13 @@ export class AddAction extends AbstractAction { } } - private getLibraryName(inputs: Input[]): string { - const libraryInput: Input = inputs.find( - (input) => input.name === 'library', - ) as Input; + private getLibraryName(inputs: CommandContext): string { + const libraryInput = inputs.get('library'); if (!libraryInput) { throw new Error('No library found in command input'); } - return libraryInput.value as string; + return libraryInput.value; } private getPackageName(library: string): string { diff --git a/actions/build.action.ts b/actions/build.action.ts index 52cd5150a..77cb6089e 100644 --- a/actions/build.action.ts +++ b/actions/build.action.ts @@ -1,7 +1,7 @@ import * as chalk from 'chalk'; import { join } from 'path'; import * as ts from 'typescript'; -import { Input } from '../commands'; +import { CommandContext } from '../commands'; import { AssetsManager } from '../lib/compiler/assets-manager'; import { getBuilder } from '../lib/compiler/helpers/get-builder'; import { getTscConfigPath } from '../lib/compiler/helpers/get-tsc-config.path'; @@ -36,19 +36,14 @@ export class BuildAction extends AbstractAction { protected readonly assetsManager = new AssetsManager(); protected readonly workspaceUtils = new WorkspaceUtils(); - public async handle(commandInputs: Input[], commandOptions: Input[]) { + public async handle( + commandInputs: CommandContext, + commandOptions: CommandContext, + ) { try { - const watchModeOption = commandOptions.find( - (option) => option.name === 'watch', - ); - const watchMode = !!(watchModeOption && watchModeOption.value); - - const watchAssetsModeOption = commandOptions.find( - (option) => option.name === 'watchAssets', - ); - const watchAssetsMode = !!( - watchAssetsModeOption && watchAssetsModeOption.value - ); + const watchMode = commandOptions.get('watch')?.value ?? false; + const watchAssetsMode = + commandOptions.get('watchAssets')?.value ?? false; await this.runBuild( commandInputs, @@ -67,19 +62,16 @@ export class BuildAction extends AbstractAction { } public async runBuild( - commandInputs: Input[], - commandOptions: Input[], + commandInputs: CommandContext, + commandOptions: CommandContext, watchMode: boolean, watchAssetsMode: boolean, isDebugEnabled = false, onSuccess?: () => void, ) { - const configFileName = commandOptions.find( - (option) => option.name === 'config', - )!.value as string; + const configFileName = commandOptions.get('config')?.value; const configuration = await this.loader.load(configFileName); - const appName = commandInputs.find((input) => input.name === 'app')! - .value as string; + const appName = commandInputs.get('app', true).value; const pathToTsconfig = getTscConfigPath( configuration, @@ -151,7 +143,7 @@ export class BuildAction extends AbstractAction { appName: string, pathToTsconfig: string, watchMode: boolean, - options: Input[], + options: CommandContext, tsOptions: ts.CompilerOptions, onSuccess: (() => void) | undefined, ) { @@ -180,7 +172,7 @@ export class BuildAction extends AbstractAction { private async runWebpack( configuration: Required, appName: string, - commandOptions: Input[], + commandOptions: CommandContext, pathToTsconfig: string, debug: boolean, watchMode: boolean, @@ -216,7 +208,7 @@ export class BuildAction extends AbstractAction { private async runTsc( watchMode: boolean, - options: Input[], + options: CommandContext, configuration: Required, pathToTsconfig: string, appName: string, @@ -229,10 +221,8 @@ export class BuildAction extends AbstractAction { this.tsConfigProvider, this.tsLoader, ); - const isPreserveWatchOutputEnabled = options.find( - (option) => - option.name === 'preserveWatchOutput' && option.value === true, - )?.value as boolean | undefined; + const isPreserveWatchOutputEnabled = + options.get('preserveWatchOutput')?.value ?? false; watchCompiler.run( configuration, pathToTsconfig, diff --git a/actions/generate.action.ts b/actions/generate.action.ts index 22c3ff012..18b1dca6b 100644 --- a/actions/generate.action.ts +++ b/actions/generate.action.ts @@ -1,6 +1,6 @@ import * as chalk from 'chalk'; import { Answers } from 'inquirer'; -import { Input } from '../commands'; +import { CommandContext } from '../commands'; import { getValueOrDefault } from '../lib/compiler/helpers/get-value-or-default'; import { AbstractCollection, @@ -21,135 +21,133 @@ import { import { AbstractAction } from './abstract.action'; export class GenerateAction extends AbstractAction { - public async handle(inputs: Input[], options: Input[]) { - await generateFiles(inputs.concat(options)); - } -} - -const generateFiles = async (inputs: Input[]) => { - const configuration = await loadConfiguration(); - const collectionOption = inputs.find( - (option) => option.name === 'collection', - )!.value as string; - const schematic = inputs.find((option) => option.name === 'schematic')! - .value as string; - const appName = inputs.find((option) => option.name === 'project')! - .value as string; - const spec = inputs.find((option) => option.name === 'spec'); - const flat = inputs.find((option) => option.name === 'flat'); - const specFileSuffix = inputs.find( - (option) => option.name === 'specFileSuffix', - ); - - const collection: AbstractCollection = CollectionFactory.create( - collectionOption || configuration.collection || Collection.NESTJS, - ); - const schematicOptions: SchematicOption[] = mapSchematicOptions(inputs); - schematicOptions.push( - new SchematicOption('language', configuration.language), - ); - const configurationProjects = configuration.projects; + public async handle(inputs: CommandContext) { + const configuration = await loadConfiguration(); - let sourceRoot = appName - ? getValueOrDefault(configuration, 'sourceRoot', appName) - : configuration.sourceRoot; + const collectionOption = inputs.get('collection')?.value; + const schematic = inputs.get('schematic', true).value; + const appName = inputs.get('project')?.value; - const specValue = spec!.value as boolean; - const flatValue = !!flat?.value; - const specFileSuffixValue = specFileSuffix!.value as string; - const specOptions = spec!.options as any; - let generateSpec = shouldGenerateSpec( - configuration, - schematic, - appName, - specValue, - specOptions.passedAsInput, - ); - let generateFlat = shouldGenerateFlat(configuration, appName, flatValue); - let generateSpecFileSuffix = getSpecFileSuffix( - configuration, - appName, - specFileSuffixValue, - ); + const collection: AbstractCollection = CollectionFactory.create( + collectionOption || configuration.collection || Collection.NESTJS, + ); + const schematicOptions: SchematicOption[] = mapSchematicOptions(inputs); + schematicOptions.push( + new SchematicOption('language', configuration.language), + ); + const configurationProjects = configuration.projects; - // If you only add a `lib` we actually don't have monorepo: true BUT we do have "projects" - // Ensure we don't run for new app/libs schematics - if (shouldAskForProject(schematic, configurationProjects, appName)) { - const defaultLabel = ' [ Default ]'; - let defaultProjectName: string = configuration.sourceRoot + defaultLabel; + let sourceRoot = appName + ? getValueOrDefault(configuration, 'sourceRoot', appName) + : configuration.sourceRoot; - for (const property in configurationProjects) { - if ( - configurationProjects[property].sourceRoot === configuration.sourceRoot - ) { - defaultProjectName = property + defaultLabel; - break; - } - } - - const projects = moveDefaultProjectToStart( + const spec = inputs.get('spec', true); + const flatValue = inputs.get('flat')?.value ?? false; + const specValue = spec.value; + const specFileSuffixValue = inputs.get('specFileSuffix')?.value; + const specOptions = spec.options; + let generateSpec = shouldGenerateSpec( configuration, - defaultProjectName, - defaultLabel, + schematic, + appName, + specValue, + specOptions.passedAsInput, ); - - const answers: Answers = await askForProjectName( - MESSAGES.PROJECT_SELECTION_QUESTION, - projects, + let generateFlat = shouldGenerateFlat(configuration, appName, flatValue); + let generateSpecFileSuffix = getSpecFileSuffix( + configuration, + appName, + specFileSuffixValue, ); - const project: string = answers.appName.replace(defaultLabel, ''); - if (project !== configuration.sourceRoot) { - sourceRoot = configurationProjects[project].sourceRoot; - } + // If you only add a `lib` we actually don't have monorepo: true BUT we do have "projects" + // Ensure we don't run for new app/libs schematics + if (shouldAskForProject(schematic, configurationProjects, appName)) { + const defaultLabel = ' [ Default ]'; + let defaultProjectName: string = configuration.sourceRoot + defaultLabel; - if (answers.appName !== defaultProjectName) { - // Only overwrite if the appName is not the default- as it has already been loaded above - generateSpec = shouldGenerateSpec( - configuration, - schematic, - answers.appName, - specValue, - specOptions.passedAsInput, - ); - generateFlat = shouldGenerateFlat( + for (const property in configurationProjects) { + if ( + configurationProjects[property].sourceRoot === + configuration.sourceRoot + ) { + defaultProjectName = property + defaultLabel; + break; + } + } + + const projects = moveDefaultProjectToStart( configuration, - answers.appNames, - flatValue, + defaultProjectName, + defaultLabel, ); - generateSpecFileSuffix = getSpecFileSuffix( - configuration, - appName, - specFileSuffixValue, + + const answers: Answers = await askForProjectName( + MESSAGES.PROJECT_SELECTION_QUESTION, + projects, ); - } - } - schematicOptions.push(new SchematicOption('sourceRoot', sourceRoot)); - schematicOptions.push(new SchematicOption('spec', generateSpec)); - schematicOptions.push(new SchematicOption('flat', generateFlat)); - schematicOptions.push( - new SchematicOption('specFileSuffix', generateSpecFileSuffix), - ); - try { - const schematicInput = inputs.find((input) => input.name === 'schematic'); - if (!schematicInput) { - throw new Error('Unable to find a schematic for this configuration'); + const project: string = answers.appName.replace(defaultLabel, ''); + if (project !== configuration.sourceRoot) { + sourceRoot = configurationProjects[project].sourceRoot; + } + + if (answers.appName !== defaultProjectName) { + // Only overwrite if the appName is not the default- as it has already been loaded above + generateSpec = shouldGenerateSpec( + configuration, + schematic, + answers.appName, + specValue, + specOptions.passedAsInput, + ); + generateFlat = shouldGenerateFlat( + configuration, + answers.appNames, + flatValue, + ); + generateSpecFileSuffix = getSpecFileSuffix( + configuration, + appName, + specFileSuffixValue, + ); + } } - await collection.execute(schematicInput.value as string, schematicOptions); - } catch (error) { - if (error && error.message) { - console.error(chalk.red(error.message)); + + schematicOptions.push(new SchematicOption('sourceRoot', sourceRoot)); + schematicOptions.push(new SchematicOption('spec', generateSpec)); + schematicOptions.push(new SchematicOption('flat', generateFlat)); + schematicOptions.push( + new SchematicOption('specFileSuffix', generateSpecFileSuffix), + ); + try { + const schematicInput = inputs.get('schematic'); + if (!schematicInput) { + throw new Error('Unable to find a schematic for this configuration'); + } + await collection.execute(schematicInput.value, schematicOptions); + } catch (error) { + if (error && error.message) { + console.error(chalk.red(error.message)); + } } } -}; +} -const mapSchematicOptions = (inputs: Input[]): SchematicOption[] => { +const mapSchematicOptions = (storage: CommandContext): SchematicOption[] => { const excludedInputNames = ['schematic', 'spec', 'flat', 'specFileSuffix']; const options: SchematicOption[] = []; - inputs.forEach((input) => { - if (!excludedInputNames.includes(input.name) && input.value !== undefined) { - options.push(new SchematicOption(input.name, input.value)); + storage.forEachEntry((commandStorageEntry) => { + if ( + !excludedInputNames.includes(commandStorageEntry.name) && + commandStorageEntry.value !== undefined + ) { + options.push( + new SchematicOption( + commandStorageEntry.name, + commandStorageEntry.value, + ), + ); } }); return options; diff --git a/actions/new.action.ts b/actions/new.action.ts index 99c3fef50..c2673375e 100644 --- a/actions/new.action.ts +++ b/actions/new.action.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; import * as inquirer from 'inquirer'; import { Answers, Question } from 'inquirer'; import { join } from 'path'; -import { Input } from '../commands'; +import { CommandContext, CommandContextEntry } from '../commands'; import { defaultGitIgnore } from '../lib/configuration/defaults'; import { AbstractPackageManager, @@ -24,33 +24,23 @@ import { normalizeToKebabOrSnakeCase } from '../lib/utils/formatting'; import { AbstractAction } from './abstract.action'; export class NewAction extends AbstractAction { - public async handle(inputs: Input[], options: Input[]) { - const directoryOption = options.find( - (option) => option.name === 'directory', - ); - const dryRunOption = options.find((option) => option.name === 'dry-run'); - const isDryRunEnabled = dryRunOption && dryRunOption.value; + public async handle(inputs: CommandContext, options: CommandContext) { + const directoryOption = options.get('directory'); + const isDryRunEnabled = options.get('dry-run')?.value ?? false; await askForMissingInformation(inputs, options); await generateApplicationFiles(inputs, options).catch(exit); - const shouldSkipInstall = options.some( - (option) => option.name === 'skip-install' && option.value === true, - ); - const shouldSkipGit = options.some( - (option) => option.name === 'skip-git' && option.value === true, - ); + const shouldSkipInstall = + options.get('skip-install')?.value ?? false; + const shouldSkipGit = options.get('skip-git')?.value ?? false; const projectDirectory = getProjectDirectory( - getApplicationNameInput(inputs)!, + getApplicationNameInput(inputs), directoryOption, ); if (!shouldSkipInstall) { - await installPackages( - options, - isDryRunEnabled as boolean, - projectDirectory, - ); + await installPackages(options, isDryRunEnabled, projectDirectory); } if (!isDryRunEnabled) { if (!shouldSkipGit) { @@ -64,86 +54,101 @@ export class NewAction extends AbstractAction { } } -const getApplicationNameInput = (inputs: Input[]) => - inputs.find((input) => input.name === 'name'); - -const getPackageManagerInput = (inputs: Input[]) => - inputs.find((options) => options.name === 'packageManager'); +const getApplicationNameInput = (inputs: CommandContext) => + inputs.get('name', true); const getProjectDirectory = ( - applicationName: Input, - directoryOption?: Input, + applicationName: CommandContextEntry, + directoryOption: CommandContextEntry | undefined, ): string => { return ( - (directoryOption && (directoryOption.value as string)) || - normalizeToKebabOrSnakeCase(applicationName.value as string) + directoryOption?.value || normalizeToKebabOrSnakeCase(applicationName.value) ); }; -const askForMissingInformation = async (inputs: Input[], options: Input[]) => { +const askForMissingInformation = async ( + inputs: CommandContext, + options: CommandContext, +) => { console.info(MESSAGES.PROJECT_INFORMATION_START); console.info(); const prompt: inquirer.PromptModule = inquirer.createPromptModule(); const nameInput = getApplicationNameInput(inputs); - if (!nameInput!.value) { + if (!nameInput.value) { const message = 'What name would you like to use for the new project?'; const questions = [generateInput('name', message)('nest-app')]; const answers: Answers = await prompt(questions as ReadonlyArray); replaceInputMissingInformation(inputs, answers); } - const packageManagerInput = getPackageManagerInput(options); - if (!packageManagerInput!.value) { + const packageManagerInput = options.get('packageManager'); + if (!packageManagerInput?.value) { const answers = await askForPackageManager(); replaceInputMissingInformation(options, answers); } }; const replaceInputMissingInformation = ( - inputs: Input[], + inputs: CommandContext, answers: Answers, -): Input[] => { - return inputs.map( - (input) => - (input.value = - input.value !== undefined ? input.value : answers[input.name]), - ); +): void => { + inputs.forEachEntry((input) => { + if (input.value === undefined) { + const maybeInputAnswer = answers[input.name]; + inputs.set({ + name: input.name, + value: maybeInputAnswer, + }); + } + }); }; -const generateApplicationFiles = async (args: Input[], options: Input[]) => { - const collectionName = options.find( - (option) => option.name === 'collection' && option.value != null, - )!.value; - const collection: AbstractCollection = CollectionFactory.create( - (collectionName as Collection) || Collection.NESTJS, - ); - const schematicOptions: SchematicOption[] = mapSchematicOptions( - args.concat(options), - ); +const generateApplicationFiles = async ( + args: CommandContext, + options: CommandContext, +) => { + const collectionName = + options.get('collection')?.value || Collection.NESTJS; + const collection: AbstractCollection = + CollectionFactory.create(collectionName); + + const argsAndOptionStorage = new CommandContext(); + argsAndOptionStorage.mergeWith(args); + argsAndOptionStorage.mergeWith(options); + const schematicOptions: SchematicOption[] = + mapSchematicOptions(argsAndOptionStorage); await collection.execute('application', schematicOptions); + console.info(); }; -const mapSchematicOptions = (options: Input[]): SchematicOption[] => { - return options.reduce( - (schematicOptions: SchematicOption[], option: Input) => { - if (option.name !== 'skip-install') { - schematicOptions.push(new SchematicOption(option.name, option.value)); - } - return schematicOptions; - }, - [], - ); +const mapSchematicOptions = (storage: CommandContext): SchematicOption[] => { + const excludedInputNames = ['skip-install']; + const options: SchematicOption[] = []; + storage.forEachEntry((commandStorageEntry) => { + if ( + !excludedInputNames.includes(commandStorageEntry.name) && + commandStorageEntry.value !== undefined + ) { + options.push( + new SchematicOption( + commandStorageEntry.name, + commandStorageEntry.value, + ), + ); + } + }); + return options; }; const installPackages = async ( - options: Input[], + options: CommandContext, dryRunMode: boolean, installDirectory: string, ) => { - const inputPackageManager = getPackageManagerInput(options)!.value as string; + const inputPackageManager = options.get('packageManager', true).value; let packageManager: AbstractPackageManager; if (dryRunMode) { diff --git a/actions/start.action.ts b/actions/start.action.ts index e50b76a1d..99e3e74fa 100644 --- a/actions/start.action.ts +++ b/actions/start.action.ts @@ -3,7 +3,7 @@ import { spawn } from 'child_process'; import * as fs from 'fs'; import { join } from 'path'; import * as killProcess from 'tree-kill'; -import { Input } from '../commands'; +import { CommandContext } from '../commands'; import { getTscConfigPath } from '../lib/compiler/helpers/get-tsc-config.path'; import { getValueOrDefault } from '../lib/compiler/helpers/get-value-or-default'; import { @@ -15,14 +15,14 @@ import { treeKillSync as killProcessSync } from '../lib/utils/tree-kill'; import { BuildAction } from './build.action'; export class StartAction extends BuildAction { - public async handle(commandInputs: Input[], commandOptions: Input[]) { + public async handle( + commandInputs: CommandContext, + commandOptions: CommandContext, + ) { try { - const configFileName = commandOptions.find( - (option) => option.name === 'config', - )!.value as string; + const configFileName = commandOptions.get('config', true).value; const configuration = await this.loader.load(configFileName); - const appName = commandInputs.find((input) => input.name === 'app')! - .value as string; + const appName = commandInputs.get('app', true).value; const pathToTsconfig = getTscConfigPath( configuration, @@ -30,20 +30,11 @@ export class StartAction extends BuildAction { appName, ); - const debugModeOption = commandOptions.find( - (option) => option.name === 'debug', - ); - const watchModeOption = commandOptions.find( - (option) => option.name === 'watch', - ); - const isWatchEnabled = !!(watchModeOption && watchModeOption.value); - const watchAssetsModeOption = commandOptions.find( - (option) => option.name === 'watchAssets', - ); - const isWatchAssetsEnabled = !!( - watchAssetsModeOption && watchAssetsModeOption.value - ); - const debugFlag = debugModeOption && debugModeOption.value; + const isWatchEnabled = + commandOptions.get('watch')?.value ?? false; + const isWatchAssetsEnabled = + commandOptions.get('watchAssets')?.value ?? false; + const debugFlag = commandOptions.get('debug')?.value ?? false; const binaryToRun = getValueOrDefault( configuration, 'exec', @@ -85,7 +76,7 @@ export class StartAction extends BuildAction { commandOptions, isWatchEnabled, isWatchAssetsEnabled, - !!debugFlag, + debugFlag, onSuccess, ); } catch (err) { diff --git a/commands/abstract.command.ts b/commands/abstract.command.ts index 97969b2c0..a5bb6bfb5 100644 --- a/commands/abstract.command.ts +++ b/commands/abstract.command.ts @@ -1,8 +1,8 @@ import { CommanderStatic } from 'commander'; import { AbstractAction } from '../actions/abstract.action'; -export abstract class AbstractCommand { - constructor(protected action: AbstractAction) {} +export abstract class AbstractCommand { + constructor(protected action: T) {} public abstract load(program: CommanderStatic): void; } diff --git a/commands/add.command.ts b/commands/add.command.ts index a2006dbef..3df1579a0 100644 --- a/commands/add.command.ts +++ b/commands/add.command.ts @@ -1,9 +1,10 @@ import { Command, CommanderStatic } from 'commander'; +import type { AddAction } from '../actions'; import { getRemainingFlags } from '../lib/utils/remaining-flags'; import { AbstractCommand } from './abstract.command'; -import { Input } from './command.input'; +import { CommandContext } from './command-context'; -export class AddCommand extends AbstractCommand { +export class AddCommand extends AbstractCommand { public load(program: CommanderStatic): void { program .command('add ') @@ -17,20 +18,24 @@ export class AddCommand extends AbstractCommand { .option('-p, --project [project]', 'Project in which to generate files.') .usage(' [options] [library-specific-options]') .action(async (library: string, command: Command) => { - const options: Input[] = []; - options.push({ name: 'dry-run', value: !!command.dryRun }); - options.push({ name: 'skip-install', value: command.skipInstall }); - options.push({ + const commandOptions = new CommandContext(); + + commandOptions.add({ name: 'dry-run', value: !!command.dryRun }); + commandOptions.add({ + name: 'skip-install', + value: command.skipInstall, + }); + commandOptions.add({ name: 'project', value: command.project, }); - const inputs: Input[] = []; - inputs.push({ name: 'library', value: library }); + const inputs = new CommandContext(); + inputs.add({ name: 'library', value: library }); const flags = getRemainingFlags(program); try { - await this.action.handle(inputs, options, flags); + await this.action.handle(inputs, commandOptions, flags); } catch (err) { process.exit(1); } diff --git a/commands/build.command.ts b/commands/build.command.ts index 1e3f03f18..2f4b27ce5 100644 --- a/commands/build.command.ts +++ b/commands/build.command.ts @@ -1,9 +1,10 @@ import { Command, CommanderStatic } from 'commander'; +import type { BuildAction } from '../actions'; import { ERROR_PREFIX, INFO_PREFIX } from '../lib/ui'; import { AbstractCommand } from './abstract.command'; -import { Input } from './command.input'; +import { CommandContext } from './command-context'; -export class BuildCommand extends AbstractCommand { +export class BuildCommand extends AbstractCommand { public load(program: CommanderStatic): void { program .command('build [app]') @@ -25,22 +26,25 @@ export class BuildCommand extends AbstractCommand { ) .description('Build Nest application.') .action(async (app: string, command: Command) => { - const options: Input[] = []; + const commandOptions = new CommandContext(); - options.push({ + commandOptions.add({ name: 'config', value: command.config, }); const isWebpackEnabled = command.tsc ? false : command.webpack; - options.push({ name: 'webpack', value: isWebpackEnabled }); - options.push({ name: 'watch', value: !!command.watch }); - options.push({ name: 'watchAssets', value: !!command.watchAssets }); - options.push({ + commandOptions.add({ name: 'webpack', value: isWebpackEnabled }); + commandOptions.add({ name: 'watch', value: !!command.watch }); + commandOptions.add({ + name: 'watchAssets', + value: !!command.watchAssets, + }); + commandOptions.add({ name: 'path', value: command.path, }); - options.push({ + commandOptions.add({ name: 'webpackPath', value: command.webpackPath, }); @@ -55,7 +59,7 @@ export class BuildCommand extends AbstractCommand { ); return; } - options.push({ + commandOptions.add({ name: 'builder', value: command.builder, }); @@ -66,12 +70,12 @@ export class BuildCommand extends AbstractCommand { ` "typeCheck" will not have any effect when "builder" is not "swc".`, ); } - options.push({ + commandOptions.add({ name: 'typeCheck', value: command.typeCheck, }); - options.push({ + commandOptions.add({ name: 'preserveWatchOutput', value: !!command.preserveWatchOutput && @@ -79,9 +83,9 @@ export class BuildCommand extends AbstractCommand { !isWebpackEnabled, }); - const inputs: Input[] = []; - inputs.push({ name: 'app', value: app }); - await this.action.handle(inputs, options); + const inputs = new CommandContext(); + inputs.add({ name: 'app', value: app }); + await this.action.handle(inputs, commandOptions); }); } } diff --git a/commands/command-context.ts b/commands/command-context.ts new file mode 100644 index 000000000..f99d75e19 --- /dev/null +++ b/commands/command-context.ts @@ -0,0 +1,71 @@ +export interface CommandContextEntry< + TValue extends boolean | string = boolean | string, +> { + readonly name: string; + readonly value: TValue; + readonly options?: any; +} + +export class CommandContext { + private readonly inputsByName = new Map< + CommandContextEntry['name'], + CommandContextEntry + >(); + + forEachEntry(callback: (input: CommandContextEntry) => void): void { + this.inputsByName.forEach((input) => callback(input)); + } + + /** + * Add a new input to the storage if it does not exist yet. + */ + add(input: CommandContextEntry) { + if (!this.inputsByName.has(input.name)) { + this.inputsByName.set(input.name, input); + } + } + + /** + * Overwrite a existing input to a new value the storage or create a new entry + * if it does not exist yet. + */ + set(input: CommandContextEntry) { + this.inputsByName.set(input.name, input); + } + + get( + inputName: CommandContextEntry['name'], + ): CommandContextEntry | undefined; + get( + inputName: CommandContextEntry['name'], + errorOnMissing: false, + ): CommandContextEntry | undefined; + get( + inputName: CommandContextEntry['name'], + errorOnMissing: true, + ): CommandContextEntry; + get( + inputName: CommandContextEntry['name'], + errorOnMissing = false, + ): CommandContextEntry | undefined { + const input = this.inputsByName.get(inputName) as + | CommandContextEntry + | undefined; + if (errorOnMissing) { + if (!input || input.value === undefined) { + throw new Error(`The input ${inputName} is missing!`); + } + } + return input; + } + + /** + * Copy all inputs of the other command storage with this one. + * Note that if an input already exists, it will **not** be overwritten. + */ + mergeWith(otherStorage: CommandContext): void { + for (const input of otherStorage.inputsByName.values()) { + this.add(input); + } + } +} diff --git a/commands/command.input.ts b/commands/command.input.ts deleted file mode 100644 index 3d490827f..000000000 --- a/commands/command.input.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Input { - name: string; - value: boolean | string; - options?: any; -} diff --git a/commands/generate.command.ts b/commands/generate.command.ts index 41a7437ca..8c107bcd9 100644 --- a/commands/generate.command.ts +++ b/commands/generate.command.ts @@ -1,13 +1,14 @@ import * as chalk from 'chalk'; import * as Table from 'cli-table3'; import { Command, CommanderStatic } from 'commander'; +import type { GenerateAction } from '../actions'; import { AbstractCollection, CollectionFactory } from '../lib/schematics'; import { Schematic } from '../lib/schematics/nest.collection'; import { loadConfiguration } from '../lib/utils/load-configuration'; import { AbstractCommand } from './abstract.command'; -import { Input } from './command.input'; +import { CommandContext } from './command-context'; -export class GenerateCommand extends AbstractCommand { +export class GenerateCommand extends AbstractCommand { public async load(program: CommanderStatic): Promise { program .command('generate [name] [path]') @@ -55,14 +56,15 @@ export class GenerateCommand extends AbstractCommand { path: string, command: Command, ) => { - const options: Input[] = []; - options.push({ name: 'dry-run', value: !!command.dryRun }); + const commandInputs = new CommandContext(); + + commandInputs.add({ name: 'dry-run', value: !!command.dryRun }); if (command.flat !== undefined) { - options.push({ name: 'flat', value: command.flat }); + commandInputs.add({ name: 'flat', value: command.flat }); } - options.push({ + commandInputs.add({ name: 'spec', value: typeof command.spec === 'boolean' @@ -75,30 +77,29 @@ export class GenerateCommand extends AbstractCommand { : command.spec.passedAsInput, }, }); - options.push({ + commandInputs.add({ name: 'specFileSuffix', value: command.specFileSuffix, }); - options.push({ + commandInputs.add({ name: 'collection', value: command.collection, }); - options.push({ + commandInputs.add({ name: 'project', value: command.project, }); - options.push({ + commandInputs.add({ name: 'skipImport', value: command.skipImport, }); - const inputs: Input[] = []; - inputs.push({ name: 'schematic', value: schematic }); - inputs.push({ name: 'name', value: name }); - inputs.push({ name: 'path', value: path }); + commandInputs.add({ name: 'schematic', value: schematic }); + commandInputs.add({ name: 'name', value: name }); + commandInputs.add({ name: 'path', value: path }); - await this.action.handle(inputs, options); + await this.action.handle(commandInputs); }, ); } diff --git a/commands/index.ts b/commands/index.ts index 8047da9f2..021a72a3b 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -1,2 +1,2 @@ export * from './command.loader'; -export * from './command.input'; +export * from './command-context'; diff --git a/commands/info.command.ts b/commands/info.command.ts index 1d968badc..f83722fbd 100644 --- a/commands/info.command.ts +++ b/commands/info.command.ts @@ -1,7 +1,8 @@ import { CommanderStatic } from 'commander'; +import type { InfoAction } from '../actions'; import { AbstractCommand } from './abstract.command'; -export class InfoCommand extends AbstractCommand { +export class InfoCommand extends AbstractCommand { public load(program: CommanderStatic) { program .command('info') diff --git a/commands/new.command.ts b/commands/new.command.ts index 28b595e1e..486eb6cd0 100644 --- a/commands/new.command.ts +++ b/commands/new.command.ts @@ -1,9 +1,10 @@ import { Command, CommanderStatic } from 'commander'; +import type { NewAction } from '../actions'; import { Collection } from '../lib/schematics'; import { AbstractCommand } from './abstract.command'; -import { Input } from './command.input'; +import { CommandContext } from './command-context'; -export class NewCommand extends AbstractCommand { +export class NewCommand extends AbstractCommand { public load(program: CommanderStatic) { program .command('new [name]') @@ -33,19 +34,29 @@ export class NewCommand extends AbstractCommand { ) .option('--strict', 'Enables strict mode in TypeScript.', false) .action(async (name: string, command: Command) => { - const options: Input[] = []; - const availableLanguages = ['js', 'ts', 'javascript', 'typescript']; - options.push({ name: 'directory', value: command.directory }); - options.push({ name: 'dry-run', value: command.dryRun }); - options.push({ name: 'skip-git', value: command.skipGit }); - options.push({ name: 'skip-install', value: command.skipInstall }); - options.push({ name: 'strict', value: command.strict }); - options.push({ + const commandOptions = new CommandContext(); + + commandOptions.add({ + name: 'directory', + value: command.directory, + }); + commandOptions.add({ name: 'dry-run', value: command.dryRun }); + commandOptions.add({ name: 'skip-git', value: command.skipGit }); + commandOptions.add({ + name: 'skip-install', + value: command.skipInstall, + }); + commandOptions.add({ name: 'strict', value: command.strict }); + commandOptions.add({ name: 'packageManager', value: command.packageManager, }); - options.push({ name: 'collection', value: command.collection }); + commandOptions.add({ + name: 'collection', + value: command.collection, + }); + const availableLanguages = ['js', 'ts', 'javascript', 'typescript']; if (!!command.language) { const lowercasedLanguage = command.language.toLowerCase(); const langMatch = availableLanguages.includes(lowercasedLanguage); @@ -66,15 +77,15 @@ export class NewCommand extends AbstractCommand { break; } } - options.push({ + commandOptions.add({ name: 'language', value: command.language, }); - const inputs: Input[] = []; - inputs.push({ name: 'name', value: name }); + const inputs = new CommandContext(); + inputs.add({ name: 'name', value: name }); - await this.action.handle(inputs, options); + await this.action.handle(inputs, commandOptions); }); } } diff --git a/commands/start.command.ts b/commands/start.command.ts index 361f48fbd..62d81dc10 100644 --- a/commands/start.command.ts +++ b/commands/start.command.ts @@ -1,10 +1,11 @@ import { Command, CommanderStatic } from 'commander'; +import type { StartAction } from '../actions'; import { ERROR_PREFIX, INFO_PREFIX } from '../lib/ui'; -import { getRemainingFlags } from '../lib/utils/remaining-flags'; import { AbstractCommand } from './abstract.command'; -import { Input } from './command.input'; +import { CommandContext } from './command-context'; +import type { BuilderVariant } from '../lib/configuration'; -export class StartCommand extends AbstractCommand { +export class StartCommand extends AbstractCommand { public load(program: CommanderStatic): void { program .command('start [app]') @@ -40,39 +41,42 @@ export class StartCommand extends AbstractCommand { ) .description('Run Nest application.') .action(async (app: string, command: Command) => { - const options: Input[] = []; + const commandOptions = new CommandContext(); - options.push({ + commandOptions.add({ name: 'config', value: command.config, }); const isWebpackEnabled = command.tsc ? false : command.webpack; - options.push({ name: 'webpack', value: isWebpackEnabled }); - options.push({ name: 'debug', value: command.debug }); - options.push({ name: 'watch', value: !!command.watch }); - options.push({ name: 'watchAssets', value: !!command.watchAssets }); - options.push({ + commandOptions.add({ name: 'webpack', value: isWebpackEnabled }); + commandOptions.add({ name: 'debug', value: command.debug }); + commandOptions.add({ name: 'watch', value: !!command.watch }); + commandOptions.add({ + name: 'watchAssets', + value: !!command.watchAssets, + }); + commandOptions.add({ name: 'path', value: command.path, }); - options.push({ + commandOptions.add({ name: 'webpackPath', value: command.webpackPath, }); - options.push({ + commandOptions.add({ name: 'exec', value: command.exec, }); - options.push({ + commandOptions.add({ name: 'sourceRoot', value: command.sourceRoot, }); - options.push({ + commandOptions.add({ name: 'entryFile', value: command.entryFile, }); - options.push({ + commandOptions.add({ name: 'preserveWatchOutput', value: !!command.preserveWatchOutput && @@ -80,7 +84,7 @@ export class StartCommand extends AbstractCommand { !isWebpackEnabled, }); - const availableBuilders = ['tsc', 'webpack', 'swc']; + const availableBuilders: BuilderVariant[] = ['tsc', 'webpack', 'swc']; if (command.builder && !availableBuilders.includes(command.builder)) { console.error( ERROR_PREFIX + @@ -90,7 +94,7 @@ export class StartCommand extends AbstractCommand { ); return; } - options.push({ + commandOptions.add({ name: 'builder', value: command.builder, }); @@ -101,17 +105,16 @@ export class StartCommand extends AbstractCommand { ` "typeCheck" will not have any effect when "builder" is not "swc".`, ); } - options.push({ + commandOptions.add({ name: 'typeCheck', value: command.typeCheck, }); - const inputs: Input[] = []; - inputs.push({ name: 'app', value: app }); - const flags = getRemainingFlags(program); + const inputs = new CommandContext(); + inputs.add({ name: 'app', value: app }); try { - await this.action.handle(inputs, options, flags); + await this.action.handle(inputs, commandOptions); } catch (err) { process.exit(1); } diff --git a/lib/compiler/helpers/get-builder.ts b/lib/compiler/helpers/get-builder.ts index 316513136..16935d631 100644 --- a/lib/compiler/helpers/get-builder.ts +++ b/lib/compiler/helpers/get-builder.ts @@ -1,4 +1,4 @@ -import { Input } from '../../../commands'; +import { CommandContext } from '../../../commands'; import { Builder, Configuration } from '../../configuration'; import { getValueOrDefault } from './get-value-or-default'; @@ -11,7 +11,7 @@ import { getValueOrDefault } from './get-value-or-default'; */ export function getBuilder( configuration: Required, - cmdOptions: Input[], + cmdOptions: CommandContext, appName: string, ) { const builderValue = getValueOrDefault( diff --git a/lib/compiler/helpers/get-tsc-config.path.ts b/lib/compiler/helpers/get-tsc-config.path.ts index 04dee58f6..1be7bd8f7 100644 --- a/lib/compiler/helpers/get-tsc-config.path.ts +++ b/lib/compiler/helpers/get-tsc-config.path.ts @@ -1,4 +1,4 @@ -import { Input } from '../../../commands'; +import { CommandContext } from '../../../commands'; import { Builder, Configuration } from '../../configuration'; import { getDefaultTsconfigPath } from '../../utils/get-default-tsconfig-path'; import { getValueOrDefault } from './get-value-or-default'; @@ -12,7 +12,7 @@ import { getValueOrDefault } from './get-value-or-default'; */ export function getTscConfigPath( configuration: Required, - cmdOptions: Input[], + cmdOptions: CommandContext, appName: string, ) { let tsconfigPath = getValueOrDefault( diff --git a/lib/compiler/helpers/get-value-or-default.ts b/lib/compiler/helpers/get-value-or-default.ts index 9b0b71c14..29506cc72 100644 --- a/lib/compiler/helpers/get-value-or-default.ts +++ b/lib/compiler/helpers/get-value-or-default.ts @@ -1,4 +1,4 @@ -import { Input } from '../../../commands'; +import { CommandContextEntry, CommandContext } from '../../../commands'; import { Configuration } from '../../configuration'; export function getValueOrDefault( @@ -14,13 +14,15 @@ export function getValueOrDefault( | 'exec' | 'builder' | 'typeCheck', - options: Input[] = [], + options: CommandContextEntry[] | CommandContext = [], defaultValue?: T, ): T { - const item = options.find((option) => option.name === key); - const origValue = item && (item.value as unknown as T); + const item = Array.isArray(options) + ? options.find((option) => option.name === key) + : key && options.get(key); + const origValue = item?.value as T | undefined; if (origValue !== undefined && origValue !== null) { - return origValue as T; + return origValue; } if (configuration.projects && configuration.projects[appName as string]) { // Wrap the application name in double-quotes to prevent splitting it diff --git a/lib/compiler/helpers/get-webpack-config-path.ts b/lib/compiler/helpers/get-webpack-config-path.ts index 08e8a58ca..738db39f0 100644 --- a/lib/compiler/helpers/get-webpack-config-path.ts +++ b/lib/compiler/helpers/get-webpack-config-path.ts @@ -1,4 +1,4 @@ -import { Input } from '../../../commands'; +import { CommandContext } from '../../../commands'; import { Builder, Configuration } from '../../configuration'; import { getValueOrDefault } from './get-value-or-default'; @@ -11,7 +11,7 @@ import { getValueOrDefault } from './get-value-or-default'; */ export function getWebpackConfigPath( configuration: Required, - cmdOptions: Input[], + cmdOptions: CommandContext, appName: string, ) { let webpackPath = getValueOrDefault( diff --git a/lib/compiler/webpack-compiler.ts b/lib/compiler/webpack-compiler.ts index db125e182..17732f539 100644 --- a/lib/compiler/webpack-compiler.ts +++ b/lib/compiler/webpack-compiler.ts @@ -1,6 +1,6 @@ import { existsSync } from 'fs'; import { join } from 'path'; -import { Input } from '../../commands'; +import { CommandContext } from '../../commands'; import { Configuration } from '../configuration'; import { INFO_PREFIX } from '../ui'; import { AssetsManager } from './assets-manager'; @@ -20,7 +20,7 @@ type WebpackConfigFactoryOrConfig = | webpack.Configuration; type WebpackCompilerExtras = { - inputs: Input[]; + inputs: CommandContext; assetsManager: AssetsManager; webpackConfigFactoryOrConfig: | WebpackConfigFactoryOrConfig diff --git a/lib/utils/project-utils.ts b/lib/utils/project-utils.ts index 5c4c6fbf9..c28a689f7 100644 --- a/lib/utils/project-utils.ts +++ b/lib/utils/project-utils.ts @@ -1,6 +1,6 @@ import * as inquirer from 'inquirer'; import { Answers, Question } from 'inquirer'; -import { Input } from '../../commands'; +import { CommandContext } from '../../commands'; import { getValueOrDefault } from '../compiler/helpers/get-value-or-default'; import { Configuration, ProjectConfiguration } from '../configuration'; import { generateSelect } from '../questions/questions'; @@ -8,7 +8,7 @@ import { generateSelect } from '../questions/questions'; export function shouldAskForProject( schematic: string, configurationProjects: { [key: string]: ProjectConfiguration }, - appName: string, + appName: string | undefined, ) { return ( ['app', 'sub-app', 'library', 'lib'].includes(schematic) === false && @@ -21,7 +21,7 @@ export function shouldAskForProject( export function shouldGenerateSpec( configuration: Required, schematic: string, - appName: string, + appName: string | undefined, specValue: boolean, specPassedAsInput?: boolean, ) { @@ -70,7 +70,7 @@ export function shouldGenerateSpec( export function shouldGenerateFlat( configuration: Required, - appName: string, + appName: string | undefined, flatValue: boolean, ): boolean { // CLI parameters have the highest priority @@ -91,8 +91,8 @@ export function shouldGenerateFlat( export function getSpecFileSuffix( configuration: Required, - appName: string, - specFileSuffixValue: string, + appName: string | undefined, + specFileSuffixValue: string | undefined, ): string { // CLI parameters have the highest priority if (specFileSuffixValue) { @@ -110,7 +110,7 @@ export function getSpecFileSuffix( if (typeof specFileSuffixConfiguration === 'string') { return specFileSuffixConfiguration; } - return specFileSuffixValue; + throw new Error('No spec file suffix was defined!'); } export async function askForProjectName( @@ -142,11 +142,9 @@ export function moveDefaultProjectToStart( export function hasValidOptionFlag( queriedOptionName: string, - options: Input[], + options: CommandContext, queriedValue: string | number | boolean = true, ): boolean { - return options.some( - (option: Input) => - option.name === queriedOptionName && option.value === queriedValue, - ); + const option = options.get(queriedOptionName); + return option?.value === queriedValue; } diff --git a/test/lib/questions/questions.spec.ts b/test/lib/questions/questions.spec.ts index 98e782807..2faaa6932 100644 --- a/test/lib/questions/questions.spec.ts +++ b/test/lib/questions/questions.spec.ts @@ -1,5 +1,5 @@ import { Question } from 'inquirer'; -import { Input } from '../../../commands/command.input'; +import { CommandContextEntry } from '../../../commands/command-context'; import { generateInput, generateSelect, @@ -8,7 +8,7 @@ import { describe('Questions', () => { describe('generateInput', () => { it('should return an input question', () => { - const input: Input = { + const input: CommandContextEntry = { name: 'name', value: 'test', };