Skip to content
This repository has been archived by the owner on Jul 8, 2024. It is now read-only.

Commit

Permalink
feat: add library and cli args to lsc create app
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafael Calpena Rodrigues committed Aug 18, 2020
1 parent 6a4c3fd commit a5debee
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 34 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 22 additions & 34 deletions lib/commands/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}

});
});
};
Expand Down
48 changes: 48 additions & 0 deletions lib/utils/bootstrap-ui-package.ts
Original file line number Diff line number Diff line change
@@ -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.`);
}
75 changes: 75 additions & 0 deletions lib/utils/create-utils.ts
Original file line number Diff line number Diff line change
@@ -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)
};
};
17 changes: 17 additions & 0 deletions lib/utils/flip-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** Transform object keys into values and vice-versa */
export const flipObject = <O extends Record<string, string>>(object: O) : Invert<O> => {
return Object.keys(object).reduce((result, key) => {
result[object[key]] = key;
return result;
}, {} as Invert<O>);
};


/* Adapted from https://stackoverflow.com/questions/56415826/is-it-possible-to-precisely-type-invert-in-typescript */
type KeyFromValue<V, T extends Record<string, string>> = {
[K in keyof T]: V extends T[K] ? K : never
}[keyof T];

type Invert<T extends Record<string, string>> = {
[V in T[keyof T]]: KeyFromValue<V, T>
};
11 changes: 11 additions & 0 deletions lib/utils/get-cli-answers.ts
Original file line number Diff line number Diff line change
@@ -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;
}, {});
};
3 changes: 3 additions & 0 deletions lib/utils/pad-left.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function padLeft(dateValue: number) {
return dateValue < 10 ? '0' + dateValue : dateValue.toString();
}
12 changes: 12 additions & 0 deletions lib/utils/spawn-sync-strict.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { spawnSync } from 'child_process';
type onErrorFn = (spawnObj: ReturnType<typeof spawnSync>, ...args: spawnSyncParameters) => number;
type spawnSyncParameters = Parameters<typeof spawnSync>;

/** 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;
};

0 comments on commit a5debee

Please sign in to comment.