apollo-elements/apollo-elements

View on GitHub
packages/create/prompt.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import type { BaseOptions, AppOptions, ComponentOptions } from './options';

import { app } from './app.js';
import { component } from './component.js';

import Case from 'case';
import inquirer from 'inquirer';

import BANNER from './banner.js';

import m from 'module';

import Yargs from 'yargs';

export type PromptOptions<T> =
  Partial<T> & BaseOptions;

const ERR_BAD_CE_TAG_NAME =
  'Custom element tag names must contain a hyphen (-)';

function normalizeOptions<T extends BaseOptions>(options: T): T {
  return Object.fromEntries(Object.entries(options).flatMap(([key, value]) => [
    [key, value],
    [Case.camel(key), options[Case.camel(key) as keyof T] ?? value],
  ])) as T;
}

export async function promptApp(options: PromptOptions<AppOptions>): Promise<AppOptions> {
  return {
    ...options,
    ...await inquirer.prompt([{
      type: 'input',
      name: 'uri',
      message: 'What is the URI to your GraphQL endpoint?',
      default: '/graphql',
    }, {
      type: 'confirm',
      name: 'overwrite',
      message: 'Overwrite existing files?',
      default: false,
    }, {
      type: 'confirm',
      name: 'package-defaults',
      message: 'Use default package.json fields (e.g. author, license, etc)?',
      default: true,
    }, {
      type: 'confirm',
      name: 'install',
      message: 'Install dependencies?',
      default: true,
    }, {
      type: 'confirm',
      name: 'start',
      message: 'Launch when ready?',
      default: true,
    }], options),
  };
}

export async function promptComponent(
  options?: PromptOptions<ComponentOptions>
): Promise<ComponentOptions> {
  return {
    ...options,
    ...await inquirer.prompt([{
      type: 'list',
      name: 'type',
      message: 'What kind of component is it?',
      choices: [
        { name: 'Query', value: 'query' },
        { name: 'Mutation', value: 'mutation' },
        { name: 'Subscription', value: 'subscription' },
      ],
    }, {
      type: 'input',
      name: 'name',
      message: 'What is the component\'s tag name?',
      validate: name => name.includes('-') || ERR_BAD_CE_TAG_NAME,
    }, {
      type: 'input',
      name: 'subdir',
      message: 'Sub directory. Leave blank to scaffold to src/components',
    }, {
      when: () => !options?.operationName && options?.edit === true,
      name: 'operationName',
      message: 'Enter your operation name e.g. AllItems',
    }, {
      type: 'editor',
      name: 'operation',
      message: 'Enter your GraphQL operation.',
      when: () => options?.edit,
    }], { ...options, subdir: options && options.subdir || '' }),
  } as ComponentOptions;
}

export async function prompt(): Promise<void> {
  const require = m.createRequire(import.meta.url);

  const { version } = require('./package.json');

  const pkgManager: 'npm'|'yarn' =
      process.argv0 === 'npm' ? process.argv0
    : process.argv0 === 'yarn' ? process.argv0
    : 'npm';

  const { argv: pargv } = Yargs(process.argv)
    .scriptName(`${pkgManager ?? 'npm'} init @apollo-elements`)
    .option('pkgManager', {
      type: 'string',
      default: pkgManager,
      description: 'Preferred package manager',
    })
    .option('schemaPath', {
      type: 'string',
      default: null,
      demandOption: false,
      description: `Optional schema path for imports. Use to import from a package or specific file.`,
    })
    .option('codegen', {
      type: 'boolean',
      default: true,
      description: 'Run codegen after scaffolding files',
    })
    .option('silent', {
      type: 'boolean',
      default: false,
      description: 'Do not log anything to stdout',
    })
    .option('directory', {
      type: 'string',
      default: process.cwd(),
      demandOption: false,
      description: 'Output directory',
    })
    .command('help', 'Display this help message', yargs => yargs.showHelp())
    .command<AppOptions>('app', 'Generate an Apollo Elements Skeleton App', yargs => void yargs
      .command('help', 'Display this help message', yargs => yargs.showHelp())
      .option('uri', {
        alias: 'u',
        type: 'string',
        description: 'URI to your GraphQL endpoint',
      })
      .option('package-defaults', {
        type: 'boolean',
        description: 'Use default package.json fields (e.g. author, license)',
      })
      .option('overwrite', {
        type: 'boolean',
        description: 'Overwrite existing files',
      })
      .option('install', {
        alias: 'i',
        type: 'boolean',
        description: 'Automatically install dependencies',
      })
      .option('start', {
        alias: 's',
        type: 'boolean',
        description: 'Launch the dev server after scaffolding',
      })
      .help())
    .command<ComponentOptions>('component', 'Generate an Apollo Element', yargs => void yargs
      .command('help', 'Display this help message', yargs => yargs.showHelp())
      .option('name', {
        alias: 'n',
        type: 'string',
        description: 'Custom element tag name',
      })
      .option('subdir', {
        alias: 'd',
        type: 'string',
        description: 'Optional subdir under src/components',
      })
      .option('type', {
        alias: 't',
        type: 'string',
        choices: ['query', 'mutation', 'subscription'],
        description: 'Element Type',
      })
      .option('overwrite', {
        type: 'boolean',
        default: false,
        description: 'Overwrite files without prompting',
      })
      .option('edit', {
        type: 'boolean',
        default: true,
        description: 'Open the default editor to define the operation',
      })
      .option('operationName', {
        alias: 'o',
        type: 'string',
        description: 'GraphQL Operation name',
      })
      .option('fields', {
        type: 'string',
        demandOption: false,
        description: 'Optional custom fields e.g. `id name picture { alt url }`',
        implies: ['operationName'],
      })
      .option('variables', {
        type: 'string',
        demandOption: false,
        description: 'Optional custom variables e.g. `input: $UpdateUserInput`',
        implies: ['operationName', 'fields'],
      })
      .option('sharedCssPath', {
        type: 'string',
        demandOption: false,
        description: `Optional path for shared CSS file. Use empty string '' to disable`,
      })
      .help())
    .help()
    .usage('npm init @apollo-elements [app|component] [help] -- [args]')
    .version(version)
    .check(({ name }) => {
      if (typeof name === 'string' && !name.includes('-'))
        throw new Error(ERR_BAD_CE_TAG_NAME);
      else
        return true;
    });

  const argv = await pargv;

  try {
    if (argv._.includes('app'))
      return await promptApp(argv).then(normalizeOptions).then(app);
    else if (argv._.includes('component'))
      return await promptComponent(argv).then(normalizeOptions).then(component);
    else {
      console.log(BANNER);
      console.log(`Generator version ${version}`);

      const { generate } = await inquirer.prompt({
        name: 'generate',
        message: 'What would you like to generate?',
        type: 'list',
        choices: [{
          name: 'App',
          value: 'app',
          short: 'Scaffold an app project',
        }, {
          name: 'Component',
          value: 'component',
          short: 'Scaffold a component for an existing app',
        }],
      });

      const commandAppendedArgv: typeof argv = {
        ...argv,
        _: [generate, ...argv._ ?? []],
      };

      switch (generate) {
        case 'app':
          return await promptApp(commandAppendedArgv)
            .then(normalizeOptions)
            .then(app);
        case 'component':
          return await promptComponent(commandAppendedArgv)
            .then(normalizeOptions)
            .then(component);
      }
    }
  } catch (error: any) {
    if (error?.command?.includes?.('build:codegen'))
      return;
    else
      console.error(error);
  }
}