From a5debee79d134fa88d33a3194a50141857dbd471 Mon Sep 17 00:00:00 2001 From: Rafael Calpena Rodrigues Date: Tue, 18 Aug 2020 02:15:11 -0300 Subject: [PATCH] feat: add library and cli args to lsc create app --- README.md | 6 +++ lib/commands/app.ts | 56 +++++++++-------------- lib/utils/bootstrap-ui-package.ts | 48 ++++++++++++++++++++ lib/utils/create-utils.ts | 75 +++++++++++++++++++++++++++++++ lib/utils/flip-object.ts | 17 +++++++ lib/utils/get-cli-answers.ts | 11 +++++ lib/utils/pad-left.ts | 3 ++ lib/utils/spawn-sync-strict.ts | 12 +++++ 8 files changed, 194 insertions(+), 34 deletions(-) create mode 100644 lib/utils/bootstrap-ui-package.ts create mode 100644 lib/utils/create-utils.ts create mode 100644 lib/utils/flip-object.ts create mode 100644 lib/utils/get-cli-answers.ts create mode 100644 lib/utils/pad-left.ts create mode 100644 lib/utils/spawn-sync-strict.ts diff --git a/README.md b/README.md index 26c6d27..4a96da4 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,12 @@ Run `lsc create app` to display a list of available templates. - ui: UI template powered by Angular and LabShare Services - cli: CLI template LabShare Services +You can also provide CLI arguments for a programmatic usage: +- `--name my-app-name` +- `--description 'some description'` +- `--type ui` (can be `ui`, `api`, or `cli`) +- `-y` (Bypass "continue?" question, useful for automated scripts) + Note: The command will add all the app's files. It is recommended to create a folder and execute the command inside that folder. ## lsc Settings diff --git a/lib/commands/app.ts b/lib/commands/app.ts index 4f9b133..7a9dc9a 100644 --- a/lib/commands/app.ts +++ b/lib/commands/app.ts @@ -6,46 +6,22 @@ import * as rename from 'gulp-rename'; import * as changeCase from 'change-case'; import {PackageUpdate} from '../../lib/package/update'; import {camelCaseTransformMerge, pascalCaseTransformMerge} from 'change-case'; +import { padLeft } from '../utils/pad-left'; +import { bootstrapUIPackage } from '../utils/bootstrap-ui-package'; +import { readArguments, defaultPrompts } from '../utils/create-utils'; + const _ = require('underscore.string'); const inquirer = require('inquirer'); -function padLeft(dateValue: number) { - return dateValue < 10 ? '0' + dateValue : dateValue.toString(); -} +export const create = function() { -const defaultAppName = process - .cwd() - .split('/') - .pop() - .split('\\') - .pop(); + /* Skip prompts if cli arguments were provided */ + let {prompts: remainingPrompts, cliAnswers} = readArguments(defaultPrompts); + inquirer.prompt(remainingPrompts).then(answers => { -export const create = function() { - const prompts = [ - { - name: 'projectType', - message: 'Which type of LabShare package do you want to create?', - type: 'list', - default: 'cli', - choices: ['cli', 'api', 'ui'], - }, - { - name: 'appName', - message: 'What is the name of your project?', - default: defaultAppName, - }, - { - name: 'appDescription', - message: 'What is the description?', - }, - { - type: 'confirm', - name: 'moveon', - message: 'Continue?', - }, - ]; + /* Extend with answers from CLI arguments */ + answers = {...answers, ...cliAnswers} - inquirer.prompt(prompts).then(answers => { if (!answers.moveon) { return; } @@ -78,6 +54,11 @@ export const create = function() { .pipe(template(answers, {interpolate: /<%=([\s\S]+?)%>/g})) .pipe( rename(file => { + /* Generate dynamic folder and file names with provided app name */ + const slugRegex = /__app-name-slug__/g; + file.basename = file.basename.replace(slugRegex, answers.appNameSlug); + file.dirname = file.dirname.replace(slugRegex, answers.appNameSlug); + if (file.basename[0] === '_') { file.basename = '.' + file.basename.slice(1); } @@ -93,6 +74,13 @@ export const create = function() { this.log.info( `Successfully created LabShare ${answers.projectType} package...`, ); + + /* Apply instructions only for ui projects meanwhile. + In the future, we could extend the features to other package types. */ + if (answers.projectType === 'ui') { + bootstrapUIPackage(answers, this); + } + }); }); }; diff --git a/lib/utils/bootstrap-ui-package.ts b/lib/utils/bootstrap-ui-package.ts new file mode 100644 index 0000000..fa7a4c2 --- /dev/null +++ b/lib/utils/bootstrap-ui-package.ts @@ -0,0 +1,48 @@ +import { sync as commandExistsSync } from 'command-exists'; +import { join } from 'path'; +import { SpawnSyncStrict } from './spawn-sync-strict'; + +const defaultSpawnOptions = { stdio: 'inherit', encoding: 'utf-8' } as const; + +export function bootstrapUIPackage(answers: any, context) { + let isGitInstalled = commandExistsSync('git'); + + /** Helper to exit process in case one sub-process fails */ + const spawnSyncStrict = SpawnSyncStrict(({ status }, a, b) => { + context.log.error(`Bootstrapping process failed because sub-task "${a} ${b.join(' ')}" did not succeed (status ${status})`); + return 1; + }); + + /* Initialize before npm i because husky requires git for setup */ + if (isGitInstalled) { + context.log.info(`Git is installed, initializing repo.`); + spawnSyncStrict('git', ['init'], defaultSpawnOptions); + } + + /* Some files should be automatically generated. Set them to read-only to avoid user manipulation */ + if (commandExistsSync('chmod')) { + const uiModulesFolder = join(process.cwd(), 'projects', `${answers.appNameSlug}-app`, 'src', 'app', 'ui-modules'); + context.log.info(`Setting files as read-only to avoid overwrite`); + spawnSyncStrict('chmod', ['444', 'menu-items.ts', 'module-routes.ts'], {...defaultSpawnOptions, cwd: uiModulesFolder }); + } + else { + context.log.info(`OS does not contain chmod command.`); + } + + /* Run NPM Install */ + context.log.info(`Installing npm dependencies.`); + spawnSyncStrict('npm', ['i'], defaultSpawnOptions); + + /* Create initial commit */ + if (isGitInstalled) { + context.log.info(`Committing initial work.`); + spawnSyncStrict('git', ['add', '-A']); + spawnSyncStrict('git', ['commit', '-m', 'chore: create lsc ui app'], defaultSpawnOptions); + } + + /* Build library for first usage in development */ + context.log.info(`Building library for development`); + spawnSyncStrict('npm', ['run', 'build:lib'], defaultSpawnOptions); + + context.log.info(`lsc create app done.`); +} diff --git a/lib/utils/create-utils.ts b/lib/utils/create-utils.ts new file mode 100644 index 0000000..d6d2059 --- /dev/null +++ b/lib/utils/create-utils.ts @@ -0,0 +1,75 @@ +import * as yargs from 'yargs'; +import { flipObject } from './flip-object'; +import { getCLIAnswers } from './get-cli-answers'; + +/** Mapping of CLI argument names to respective prompt names */ +export const argsToPromptNames = { + type: 'projectType', + name: 'appName', + description: 'appDescription', + y: 'moveon' +} as const; + +/** Inverse mapping of argsToPromptNames */ +const promptNamesToArgs = flipObject(argsToPromptNames); + +/** Available CLI arguments for programmatic use. */ +const defaultCLIArgs = yargs.options({ + type: { type: 'string', choices: ['cli', 'api', 'ui'] }, + name: { type: 'string' }, + y: { type: 'boolean' } +}).argv; + +/** Default App Name based on CWD */ +const defaultAppName = process + .cwd() + .split('/') + .pop() + .split('\\') + .pop(); + +/** Prompt options */ +export const defaultPrompts = [ + { + name: 'projectType', + message: 'Which type of LabShare package do you want to create?', + type: 'list', + default: 'cli', + choices: ['cli', 'api', 'ui'], + }, + { + name: 'appName', + message: 'What is the name of your project?', + default: defaultAppName, + }, + { + name: 'appDescription', + message: 'What is the description?', + }, + { + type: 'confirm', + name: 'moveon', + message: 'Continue?', + }, +] as const; + +export const getPromptNameFor = (key: keyof typeof argsToPromptNames) => { + return argsToPromptNames[key]; +}; + +/** Decides if a prompt should be showed to the user. + * Returns false in case the equivalent cli argument has been provided. + */ +const shouldPrompt = (cliArgs) => (prompt) => { + const { name } = prompt; + const arg = promptNamesToArgs[name]; + return (typeof arg === 'string') ? !cliArgs[arg] : true; +}; + +/** Read arguments provided in the CLI and return remaining prompts to be used. */ +export const readArguments = (prompts: typeof defaultPrompts) => { + return { + prompts: prompts.filter(shouldPrompt(defaultCLIArgs)), + cliAnswers: getCLIAnswers(defaultCLIArgs) + }; +}; diff --git a/lib/utils/flip-object.ts b/lib/utils/flip-object.ts new file mode 100644 index 0000000..fc2c705 --- /dev/null +++ b/lib/utils/flip-object.ts @@ -0,0 +1,17 @@ +/** Transform object keys into values and vice-versa */ +export const flipObject = >(object: O) : Invert => { + return Object.keys(object).reduce((result, key) => { + result[object[key]] = key; + return result; + }, {} as Invert); +}; + + +/* Adapted from https://stackoverflow.com/questions/56415826/is-it-possible-to-precisely-type-invert-in-typescript */ +type KeyFromValue> = { + [K in keyof T]: V extends T[K] ? K : never +}[keyof T]; + +type Invert> = { + [V in T[keyof T]]: KeyFromValue +}; diff --git a/lib/utils/get-cli-answers.ts b/lib/utils/get-cli-answers.ts new file mode 100644 index 0000000..546b86e --- /dev/null +++ b/lib/utils/get-cli-answers.ts @@ -0,0 +1,11 @@ +import { argsToPromptNames, getPromptNameFor } from "./create-utils"; + +/** Return CLI answers mapped to prompt names */ +export const getCLIAnswers = (cliArgs) => { + return Object.keys(cliArgs).filter( + k => k in argsToPromptNames + ).reduce((obj, key: keyof typeof argsToPromptNames) => { + obj[getPromptNameFor(key)] = cliArgs[key]; + return obj; + }, {}); +}; diff --git a/lib/utils/pad-left.ts b/lib/utils/pad-left.ts new file mode 100644 index 0000000..1ccc210 --- /dev/null +++ b/lib/utils/pad-left.ts @@ -0,0 +1,3 @@ +export function padLeft(dateValue: number) { + return dateValue < 10 ? '0' + dateValue : dateValue.toString(); +} diff --git a/lib/utils/spawn-sync-strict.ts b/lib/utils/spawn-sync-strict.ts new file mode 100644 index 0000000..7ea9da7 --- /dev/null +++ b/lib/utils/spawn-sync-strict.ts @@ -0,0 +1,12 @@ +import { spawnSync } from 'child_process'; +type onErrorFn = (spawnObj: ReturnType, ...args: spawnSyncParameters) => number; +type spawnSyncParameters = Parameters; + +/** Wraps spawnSync to abort main process in case sub process fails */ +export const SpawnSyncStrict = (onError: onErrorFn = () => 1) => (...args: spawnSyncParameters) => { + let wrappedCall = spawnSync(...args); + if (wrappedCall.status !== 0) { + process.exit(onError(wrappedCall, ...args)); + } + return wrappedCall; +};