Source

cli-module.ts

import { Command } from 'commander';
import { runCommand } from '@decaf-ts/utils';
import { Logging } from '@decaf-ts/logging';


enum Projects {
  FOR_ANGULAR = "for-angular",
  FOR_ANGULAR_APP = "for-angular-app",
  LIB = "lib",
  APP = "app"
};

enum Types {
  PAGE = "page",
  SERVICE = "service",
  COMPONENT = "component",
  DIRECTIVE = "directive",
  SCHEMATICS = "schematics"
};

const logger = Logging.for("for-angular-cli");

/**
 * Creates and returns a Command object for the Angular CLI module in decaf-ts.
 * This function sets up a 'generate' command that can create various Angular artifacts.
 *
 * @returns {Command} A Command object configured with the 'generate' subcommand and its action.
 *
 * The command syntax is: generate <type> <name> [project]
 * @param {Types} type - The type of artifact to generate (e.g., service, component, directive, page).
 * @param {string} name - The name of the artifact to be generated.
 * @param {Projects} [project=Projects.FOR_ANGULAR] - The project for which to generate the artifact.
 *                   Defaults to the main Angular project if not specified.
 *
 * @throws {Error} If an invalid type is provided.
 *
 * @example
 * // Usage in CLI
 * // decaf-ts generate service my-service
 * // decaf-ts generate component my-component for-angular-app
 */
export default function angular() {
  return new Command()
    .name('decaf generate')
    .command('generate <type> <name> [project]')
    .description(`decaf-ts Angular CLI module`)
    .action(async(type: Types, name: string, project: Projects = Projects.FOR_ANGULAR) => {
      if(!validateType(type))
        return logger.error(`${type} is not valid. Use service, component or directive.`)

      if(type === Types.SCHEMATICS)
        return await generateSchematics();

      if(type === Types.PAGE) {
        logger.info(`Pages can be only generate for app. Forcing project to: ${Projects.FOR_ANGULAR_APP}`);
        project = Projects.FOR_ANGULAR_APP;
      }

      (project as string) = parseProjectName(project);

      if(!validateProject(project))
        project = Projects.FOR_ANGULAR;
      const command = project === Projects.FOR_ANGULAR_APP ?
        'ionic generate' : `ng generate --project=${Projects.FOR_ANGULAR} --path=src/lib/${type}s`;

      try {
        const result = await execute(`${command} ${type} ${name}`);
        logger.info(result as string);
      } catch(error: any) {
        logger.error(error?.message || error);
      }
    });
}


async function generateSchematics() {
  return Promise.all([
    execute(`npm link schematics`),
    execute(`cd schematics`),
    execute(`npm install`),
    execute(`npm run build`),
    execute(`npx schematics .:schematics --name=decaf`)
  ])
  .then(res => res)
  .catch(error => error)
}


/**
 * Executes a shell command asynchronously.
 *
 * @param command - The shell command to execute.
 * @returns A Promise that resolves with the command's stdout output as a string if successful,
 *          or rejects with an error message if the command fails or produces stderr output.
 */
async function execute(command: string): Promise<string|void> {
  try {
    return await runCommand(command).promise;
  } catch (error: any) {
    logger.error(error?.message || error);
  }
}

/**
 * Parses and normalizes the project name input.
 *
 * @param value - The input project name to be parsed.
 *                Can be 'app', 'lib', or any other string value.
 * @returns A normalized string representation of the project name.
 *          Returns 'for-angular-app' if input is 'app',
 *          'for-angular' if input is 'lib',
 *          or the lowercase version of the input for any other value.
 */
function parseProjectName(value: string): string {
  return (value === Projects.APP ?
    Projects.FOR_ANGULAR_APP : value === Projects.LIB ?
      Projects.FOR_ANGULAR : value).toLowerCase();
}

/**
 * Validates if the given type value is a valid enum member of Types.
 *
 * @param value - The type value to validate.
 * @returns A boolean indicating whether the value is a valid Types enum member.
 */
function validateType(value: Types): boolean {
  return Object.values(Types).includes(value);
}


/**
 * Validates if the given project value is a valid enum member of Projects.
 *
 * @param value - The project value to validate.
 * @returns A boolean indicating whether the value is a valid Projects enum member.
 */
function validateProject(value: string): boolean {
  return Object.values(Projects).includes(value as Projects);
}