Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add experimental CLI for LoopBack 4 #693

Merged
merged 1 commit into from
Nov 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
packages/*/dist
packages/*/dist6
packages/*/api-docs
packages/cli/generators/*/templates
package.json
packages/*/package.json
2 changes: 2 additions & 0 deletions packages/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
coverage
1 change: 1 addition & 0 deletions packages/cli/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
6 changes: 6 additions & 0 deletions packages/cli/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"bracketSpacing": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "es5"
}
2 changes: 2 additions & 0 deletions packages/cli/.yo-rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
57 changes: 57 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# @loopback/cli

This module contains the experimental CLI for LoopBack 4.

## Installation

Run the following command to install the CLI.

`npm install -g @loopback/cli`

## Usage

1. To scaffold a LoopBack 4 application

`lb4 app`

```
Usage:
lb4 app [options] [<name>]

Options:
-h, --help # Print the generator's options and usage
--skip-cache # Do not remember prompt answers Default: false
--skip-install # Do not automatically install dependencies Default: false
--applicationName # Application name
--description # Description for the application
--outdir # Project root directory for the application
--tslint # Enable tslint
--prettier # Enable prettier
--mocha # Enable mocha
--loopbackBuild # Use @loopback/build

Arguments:
name # Project name for the application Type: String Required: false
```

2. To scaffold a LoopBack 4 extension

`lb4 extension`

```
Usage:
lb4 extension [options] [<name>]

Options:
-h, --help # Print the generator's options and usage
--skip-cache # Do not remember prompt answers Default: false
--skip-install # Do not automatically install dependencies Default: false
--description # Description for the extension
--outdir # Project root directory for the extension
--tslint # Enable tslint
--prettier # Enable prettier
--mocha # Enable mocha
--loopbackBuild # Use @loopback/build
--componentName # Component name

```
65 changes: 65 additions & 0 deletions packages/cli/bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env node
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: @loopback/cli
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';

const assert = require('assert');
const camelCaseKeys = require('camelcase-keys');
const debug = require('debug')('loopback:cli');
const minimist = require('minimist');
const path = require('path');
const yeoman = require('yeoman-environment');

const opts = minimist(process.argv.slice(2), {
alias: {
help: 'h',
version: 'v',
commands: 'l',
},
});

if (opts.version) {
const ver = require('../package.json').version;
console.log('Version: %s', ver);
return;
}

var env = yeoman.createEnv();

env.register(path.join(__dirname, '../generators/app'), 'loopback4:app');
env.register(path.join(__dirname, '../generators/extension'), 'loopback4:extension');

// list generators
if (opts.commands) {
console.log('Available commands: ');
var list = Object.keys(env.getGeneratorsMeta())
.filter(name => /^loopback4:/.test(name))
.map(name => name.replace(/^loopback4:/, ' lb4 '));
console.log(list.join('\n'));
return;
}

const args = opts._;
const originalCommand = args.shift();
let command = 'loopback4:' + (originalCommand || 'app');
const supportedCommands = env.getGeneratorsMeta();

if (!(command in supportedCommands)) {
command = 'loopback4:app';
args.unshift(originalCommand);
args.unshift(command);
} else {
args.unshift(command);
}

debug('invoking generator', args);

// `yo` is adding flags converted to CamelCase
const options = camelCaseKeys(opts, {exclude: ['--', /^\w$/, 'argv']});
Object.assign(options, opts);

debug('env.run %j %j', args, options);
env.run(args, options);
78 changes: 78 additions & 0 deletions packages/cli/generators/app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: @loopback/cli
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';
const ProjectGenerator = require('../../lib/project-generator');
const utils = require('../../lib/utils');

module.exports = class extends ProjectGenerator {
// Note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);
}

_setupGenerator() {
this.projectType = 'application';
this.option('applicationName', {
type: String,
description: 'Application name',
});
return super._setupGenerator();
}

setOptions() {
return super.setOptions();
}

promptProjectName() {
return super.promptProjectName();
}

promptProjectDir() {
return super.promptProjectDir();
}

promptApplication() {
const prompts = [
{
type: 'input',
name: 'applicationName',
message: 'Application class name:',
default: utils.toClassName(this.projectInfo.name) + 'Application',
},
];

return this.prompt(prompts).then(props => {
Object.assign(this.projectInfo, props);
});
}

promptOptions() {
return super.promptOptions();
}

scaffold() {
return super.scaffold();
}

install() {
return super.install();
}

end() {
this.log();
this.log(
'Application %s is now created in %s.',
this.projectInfo.name,
this.projectInfo.outdir
);
this.log();
this.log('Next steps:');
this.log();
this.log('$ cd ' + this.projectInfo.outdir);
this.log('$ npm start');
this.log();
}
};
14 changes: 14 additions & 0 deletions packages/cli/generators/app/templates/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: <%= project.name %>
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

const nodeMajorVersion = +process.versions.node.split('.')[0];
const dist = nodeMajorVersion >= 7 ? './dist' : './dist6';

const application = (module.exports = require(dist));

if (require.main === module) {
// Run the application
application.main();
}
27 changes: 27 additions & 0 deletions packages/cli/generators/app/templates/src/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: <%= project.name %>
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Application, ApplicationConfig} from '@loopback/core';
import {RestComponent} from '@loopback/rest';
import {PingController} from './controllers/ping-controller';

export class <%= project.applicationName %> extends Application {
constructor(options?: ApplicationConfig) {
// Allow options to replace the defined components array, if desired.
options = Object.assign(
{},
{
components: [RestComponent],
},
options,
);
super(options);
this.setupControllers();
}

setupControllers() {
this.controller(PingController);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Controllers

This directory contains source files for the controllers exported by this app.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: <%= project.name %>
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {get, ServerRequest} from '@loopback/rest';
import {inject} from '@loopback/context';

/**
* A simple controller to bounce back http requests
*/
export class PingController {
constructor(@inject('rest.http.request') private req: ServerRequest) {}

// Map to `GET /ping`
@get('/ping')
ping(): object {
return {
greeting: 'Hello from LoopBack',
date: new Date(),
url: this.req.url,
headers: Object.assign({}, this.req.headers),
Copy link
Contributor

@b-admike b-admike Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found including the headers to be a bit ugly for UX, but feel free to keep it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, I have a json viewer extension installed for my browser and it looks fine :-). Surprisingly, most browsers don't render json response nicely by default.

};
}
}
25 changes: 25 additions & 0 deletions packages/cli/generators/app/templates/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: <%= project.name %>
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {<%= project.applicationName %>} from './application';
import {RestServer} from '@loopback/rest';
import {ApplicationConfig} from '@loopback/core';

export {<%= project.applicationName %>};

export async function main(options?: ApplicationConfig) {
const app = new <%= project.applicationName %>(options);

try {
await app.start();
const server = await app.getServer(RestServer);
const port = await server.get('rest.port');
console.log(`Server is running at http://127.0.0.1:${port}`);
console.log(`Try http://127.0.0.1:${port}/ping`);
} catch (err) {
console.error(`Unable to start application: ${err}`);
}
return app;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Repositories
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mixin, Providers, etc.. pages have a great deal of description. Can we add some links to our docs or brief explanations here as well?

Copy link
Contributor Author

@raymondfeng raymondfeng Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The READMEs are copied from https://github.com/strongloop/loopback4-extension-starter.

@virkt25 Do you make the docs for Mixin/Provider/Decorator available to http://loopback.io/doc/en/lb4/Concepts.html?

We should only keep links in the README.md to be scaffolded into user projects.


This directory contains code for repositories provided by this app.
4 changes: 4 additions & 0 deletions packages/cli/generators/app/templates/test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Tests

Please place your tests in this folder.

46 changes: 46 additions & 0 deletions packages/cli/generators/app/templates/test/ping-controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: <%= project.name %>
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {createClientForHandler, supertest} from '@loopback/testlab';
import {RestServer} from '@loopback/rest';
import {<%= project.applicationName %>} from '../';

describe('PingController', () => {
let app: <%= project.applicationName %>;
let server: RestServer;
let client: supertest.SuperTest<supertest.Test>;

before(givenAnApplication);

before(givenARestServer);

before(async () => {
await app.start();
});

before(() => {
client = createClientForHandler(server.handleHttp);
});

after(async () => {
await app.stop();
});

it('invokes GET /ping', async () => {
await client.get('/ping?msg=world').expect(200);
});

function givenAnApplication() {
app = new <%= project.applicationName %>({
rest: {
port: 0,
},
});
}

async function givenARestServer() {
server = await app.getServer(RestServer);
}
});
Loading