Skip to content

Commit

Permalink
fixup! apply feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
virkt25 committed Feb 6, 2018
1 parent 28de2e9 commit a45d535
Show file tree
Hide file tree
Showing 15 changed files with 148 additions and 77 deletions.
16 changes: 13 additions & 3 deletions packages/boot/src/boot.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {BootStrapper} from './boot-strapper';
import {Bootstrapper} from './bootstrapper';
import {Component, Application, CoreBindings} from '@loopback/core';
import {inject, BindingScope} from '@loopback/context';
import {ControllerBooter} from './booters';

/**
* BootComponent is used to export the default list of Booter's made
* available by this module as well as bind the BootStrapper to the app so it
* can be used to run the Booters.
*/
export class BootComponent implements Component {
// Export a list of default booters in the component so they get bound
// automatically when this component is mounted.
booters = [ControllerBooter];

/**
*
* @param app Application instance
*/
constructor(@inject(CoreBindings.APPLICATION_INSTANCE) app: Application) {
// Bound as a SINGLETON so it can be cached as it has no state
app
.bind(CoreBindings.BOOT_STRAPPER)
.toClass(BootStrapper)
.bind(CoreBindings.BOOTSTRAPPER)
.toClass(Bootstrapper)
.inScope(BindingScope.SINGLETON);
}
}
28 changes: 22 additions & 6 deletions packages/boot/src/booters/booter-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const glob = promisify(require('glob'));
* @param extensions An array of extensions to search for
* @param nested A boolean to determine if nested folders in dirs should be searched
* @param root Root folder to resolve dirs relative to
* @returns {string[]} Array of discovered files
*/
export async function discoverFiles(
dirs: string[],
Expand All @@ -34,6 +35,7 @@ export async function discoverFiles(
*
* @param pattern A glob pattern
* @param root Root folder to start searching for matching files
* @returns {string[]} Array of discovered files
*/
export async function discoverFilesWithGlob(
pattern: string,
Expand All @@ -42,22 +44,36 @@ export async function discoverFilesWithGlob(
return await glob(pattern, {root: root});
}

/**
* Given a function, returns true if it is a class, false otherwise.
*
* @param target The function to check if it's a class or not.
* @returns {boolean} True if target is a class. False otherwise.
*/
// tslint:disable-next-line:no-any
export function isClass(target: Constructor<any>): boolean {
return (
typeof target === 'function' && target.toString().indexOf('class') === 0
);
}

/**
* Returns an Array of Classes from given files
*
* @param files An array of string of absolute file paths
* @returns {Promise<Array<Constructor<any>>>} An array of Class Construtors from a file
*/
// tslint:disable-next-line:no-any
export async function loadClassesFromFiles(files: string[]): Promise<any[]> {
export async function loadClassesFromFiles(
files: string[],
// tslint:disable-next-line:no-any
): Promise<Array<Constructor<any>>> {
// tslint:disable-next-line:no-any
const classes: Constructor<any>[] = [];
const classes: Array<Constructor<any>> = [];
files.forEach(file => {
const ctrl = require(file);
Object.keys(ctrl).forEach(cls => {
if (
typeof ctrl[cls] === 'function' &&
ctrl[cls].toString().indexOf('class') === 0
) {
if (isClass(ctrl[cls])) {
classes.push(ctrl[cls]);
}
});
Expand Down
12 changes: 5 additions & 7 deletions packages/boot/src/booters/controller.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
* artifacts for LoopBack 4 Applications.
*
* It supports the following boot phases: config, discover, boot
*
* @param app Application instance
* @param bootConfig Config options for boot
*/
export class ControllerBooter implements Booter {
options: ControllerOptions;
Expand All @@ -27,11 +30,6 @@ export class ControllerBooter implements Booter {
extensions: string[];
discovered: string[];

/**
*
* @param app Application instance
* @param bootConfig Config options for boot
*/
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE) public app: Application,
@inject(BootBindings.BOOT_OPTIONS) public bootConfig: BootOptions,
Expand Down Expand Up @@ -105,8 +103,8 @@ export class ControllerBooter implements Booter {
* files in dirs. Defaults to ['.controller.js']
* @param nested Boolean to control if artifact discovery should check nested
* folders or not. Default to true
* @param discovered An array of discovered files. This is set by the
* discover phase.
* @param glob A `glob` string to use when searching for files. This takes
* precendence over other options.
*/
export type ControllerOptions = {
dirs: string | string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,16 @@ import {BootBindings} from './keys';
import * as debugModule from 'debug';
const debug = debugModule('loopback:boot:bootstrapper');

export class BootStrapper {
/**
* The Bootstrapper class provides the `boot` function that is responsible for
* finding and executing the Booters in an application based on given options.
*
* NOTE: Bootstrapper should be bound as a SINGLETON so it can be cached as
* it does not maintain any state of it's own.
*
* @param app Appliaction instance
*/
export class Bootstrapper {
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE) private app: Application,
) {}
Expand All @@ -35,6 +44,12 @@ export class BootStrapper {
* complete before the next phase is started.
* @param {BootOptions} bootOptions Options for boot. Bound for Booters to
* receive via Dependency Injection.
* @param {Context} [ctx] Optional Context to use to resolve bindings. This is
* primarily useful when running app.boot() again but with different settings
* (in particular phases) such as 'start' / 'stop'. Using a returned Context from
* a previous boot call allows DI to retrieve the same instances of Booters previously
* used as they are bound using a CONTEXT scope. This is important as Booter instances
* may maintain state.
*/
async boot(bootOptions: BootOptions, ctx?: Context): Promise<Context> {
if (!bootOptions.projectRoot) {
Expand All @@ -58,7 +73,7 @@ export class BootStrapper {
// and then resolving the bindings to get instances.
const bindings = bootCtx.findByTag(CoreBindings.BOOTER_TAG);
const booterInsts = await resolveList(bindings, binding =>
Promise.resolve(bootCtx.get(binding.key)),
bootCtx.get(binding.key),
);

// Determine the phases to be run. If a user set a phases filter, those
Expand All @@ -79,14 +94,13 @@ export class BootStrapper {
// Run phases of booters
for (const phase of phases) {
for (const inst of booterInsts) {
// Run phases if instance name is whitelisted.
// NOTE: Might need to polyfill .includes()
if (names.includes(inst.constructor.name)) {
const instName = inst.constructor.name;
if (names.includes(instName)) {
if (inst[phase]) {
await inst[phase]();
debug(`${inst.constructor.name} phase: ${phase} complete.`);
debug(`${instName} phase: ${phase} complete.`);
} else {
debug(`${inst.constructor.name} phase: ${phase} not implemented.`);
debug(`${instName} phase: ${phase} not implemented.`);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/boot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
// License text available at https://opensource.org/licenses/MIT

export * from './booters';
export * from './boot-strapper';
export * from './bootstrapper';
export * from './boot.component';
export * from './keys';
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {resolve} from 'path';
describe('controller booter acceptance tests', () => {
// tslint:disable-next-line:no-any
let app: any;
const SANDBOX_PATH = resolve(__dirname, '../sandbox');
const SANDBOX_PATH = resolve(__dirname, '../../.sandbox');
const sandbox = new TestSandbox(SANDBOX_PATH);

beforeEach(resetSandbox);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {ControllerBooter, ControllerDefaults} from '../../index';
import {resolve} from 'path';

describe('controller booter intengration tests', () => {
const SANDBOX_PATH = resolve(__dirname, '../sandbox');
const SANDBOX_PATH = resolve(__dirname, '../../.sandbox');
const sandbox = new TestSandbox(SANDBOX_PATH);

// tslint:disable-next-line:no-any
Expand Down
6 changes: 3 additions & 3 deletions packages/boot/test/unit/boot.component.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import {expect} from '@loopback/testlab';
import {Application, Booter, CoreBindings} from '@loopback/core';
import {Binding, Context} from '@loopback/context';
import {BootComponent, BootBindings, BootStrapper} from '../../index';
import {BootComponent, BootBindings, Bootstrapper} from '../../index';

describe('boot.component unit tests', () => {
let app: Application;
Expand All @@ -16,8 +16,8 @@ describe('boot.component unit tests', () => {
beforeEach(getBootComponent);

it('binds BootStrapper class', async () => {
const bootstrapper = await app.get(CoreBindings.BOOT_STRAPPER);
expect(bootstrapper).to.be.instanceOf(BootStrapper);
const bootstrapper = await app.get(CoreBindings.BOOTSTRAPPER);
expect(bootstrapper).to.be.instanceOf(Bootstrapper);
});

function getApp() {
Expand Down
46 changes: 23 additions & 23 deletions packages/boot/test/unit/booters/controller.booter.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {ControllerBooter, ControllerDefaults} from '../../../index';
import {resolve, relative} from 'path';

describe('controller booter unit tests', () => {
const SANDBOX_PATH = resolve(__dirname, '../../sandbox');
const SANDBOX_PATH = resolve(__dirname, '../../../.sandbox');
const sandbox = new TestSandbox(SANDBOX_PATH);

let app: Application;
Expand Down Expand Up @@ -92,11 +92,11 @@ describe('controller booter unit tests', () => {
describe('ControllerBooter.discover()', () => {
it('discovers correct files based on ControllerDefaults', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'controllers/hello.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/nested/two.controller.js',
);
const bootOptions: BootOptions = {
Expand All @@ -118,7 +118,7 @@ describe('controller booter unit tests', () => {

it('discovers correct files based on a glob pattern', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'ctrl/hello.ctrl.js',
);

Expand All @@ -138,11 +138,11 @@ describe('controller booter unit tests', () => {

it('discovers files without going into nested folders', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'controllers/hello.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/nested/two.controller.js',
);
const bootOptions: BootOptions = {
Expand All @@ -163,11 +163,11 @@ describe('controller booter unit tests', () => {

it('discovers files of specified extensions', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'controllers/hello.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/two.ctrl.js',
);
const bootOptions: BootOptions = {
Expand All @@ -186,7 +186,7 @@ describe('controller booter unit tests', () => {

it('discovers files in specified directory', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'ctrl/hello.controller.js',
);
const bootOptions: BootOptions = {
Expand All @@ -205,11 +205,11 @@ describe('controller booter unit tests', () => {

it('discovers files of multiple extensions', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'controllers/hello.ctrl.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/two.controller.js',
);
const bootOptions: BootOptions = {
Expand All @@ -231,11 +231,11 @@ describe('controller booter unit tests', () => {

it('discovers files in multiple directories', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'ctrl/hello.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/two.controller.js',
);
const bootOptions: BootOptions = {
Expand Down Expand Up @@ -273,7 +273,7 @@ describe('controller booter unit tests', () => {

it('discovers no files of an invalid extension', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/two.controller.js',
);
const bootOptions: BootOptions = {
Expand Down Expand Up @@ -309,7 +309,7 @@ describe('controller booter unit tests', () => {
describe('ControllerBooter.load()', () => {
it('binds a controller from discovered file', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'controllers/hello.controller.js',
);
const bootOptions: BootOptions = {
Expand All @@ -331,19 +331,19 @@ describe('controller booter unit tests', () => {

it('binds controllers from multiple files', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js'),
resolve(__dirname, '../../fixtures/hello.controller.js'),
'controllers/hello.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/hello.controller.js.map'),
resolve(__dirname, '../../fixtures/hello.controller.js.map'),
'controllers/hello.controller.js.map',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/nested/two.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js.map'),
resolve(__dirname, '../../fixtures/two.controller.js.map'),
'controllers/nested/two.controller.js.map',
);
const bootOptions: BootOptions = {
Expand All @@ -370,11 +370,11 @@ describe('controller booter unit tests', () => {

it('binds multiple controllers from a file', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js'),
resolve(__dirname, '../../fixtures/two.controller.js'),
'controllers/two.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/two.controller.js.map'),
resolve(__dirname, '../../fixtures/two.controller.js.map'),
'controllers/two.controller.js.map',
);
const bootOptions: BootOptions = {
Expand All @@ -399,11 +399,11 @@ describe('controller booter unit tests', () => {

it('does not throw on an empty file', async () => {
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/empty.controller.js'),
resolve(__dirname, '../../fixtures/empty.controller.js'),
'controllers/empty.controller.js',
);
await sandbox.copyFile(
resolve(SANDBOX_PATH, '../fixtures/empty.controller.js.map'),
resolve(__dirname, '../../fixtures/empty.controller.js.map'),
'controllers/empty.controller.js.map',
);
const bootOptions: BootOptions = {
Expand Down
Loading

0 comments on commit a45d535

Please sign in to comment.