UNPKG

4.92 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,2019. All Rights Reserved.
2// Node module: @loopback/boot
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 Application,
8 Context,
9 CoreBindings,
10 inject,
11 resolveList,
12} from '@loopback/core';
13import debugModule from 'debug';
14import {resolve} from 'path';
15import {BootBindings, BootTags} from './keys';
16import {bindBooter} from './mixins';
17import {
18 Bootable,
19 BOOTER_PHASES,
20 BootExecutionOptions,
21 BootOptions,
22} from './types';
23
24const debug = debugModule('loopback:boot:bootstrapper');
25
26/**
27 * The Bootstrapper class provides the `boot` function that is responsible for
28 * finding and executing the Booters in an application based on given options.
29 *
30 * NOTE: Bootstrapper should be bound as a SINGLETON so it can be cached as
31 * it does not maintain any state of it's own.
32 *
33 * @param app - Application instance
34 * @param projectRoot - The root directory of the project, relative to which all other paths are resolved
35 * @param bootOptions - The BootOptions describing the conventions to be used by various Booters
36 */
37export class Bootstrapper {
38 constructor(
39 @inject(CoreBindings.APPLICATION_INSTANCE)
40 private app: Application & Bootable,
41 @inject(BootBindings.PROJECT_ROOT) private projectRoot: string,
42 @inject(BootBindings.BOOT_OPTIONS, {optional: true})
43 private bootOptions: BootOptions = {},
44 ) {
45 // Resolve path to projectRoot and re-bind
46 this.projectRoot = resolve(this.projectRoot);
47 app.bind(BootBindings.PROJECT_ROOT).to(this.projectRoot);
48
49 // This is re-bound for testing reasons where this value may be passed directly
50 // and needs to be propagated to the Booters via DI
51 app.bind(BootBindings.BOOT_OPTIONS).to(this.bootOptions);
52 }
53
54 /**
55 * Function is responsible for calling all registered Booter classes that
56 * are bound to the Application instance. Each phase of an instance must
57 * complete before the next phase is started.
58 *
59 * @param execOptions - Execution options for boot. These
60 * determine the phases and booters that are run.
61 * @param ctx - Optional Context to use to resolve bindings. This is
62 * primarily useful when running app.boot() again but with different settings
63 * (in particular phases) such as 'start' / 'stop'. Using a returned Context from
64 * a previous boot call allows DI to retrieve the same instances of Booters previously
65 * used as they are bound using a CONTEXT scope. This is important as Booter instances
66 * may maintain state.
67 */
68 async boot(
69 execOptions?: BootExecutionOptions,
70 ctx?: Context,
71 ): Promise<Context> {
72 const bootCtx = ctx ?? new Context(this.app);
73
74 // Bind booters passed in as a part of BootOptions
75 // We use _bindBooter so this Class can be used without the Mixin
76 if (execOptions?.booters) {
77 execOptions.booters.forEach(booter => bindBooter(this.app, booter));
78 }
79
80 // Determine the phases to be run. If a user set a phases filter, those
81 // are selected otherwise we run the default phases (BOOTER_PHASES).
82 const phases = execOptions?.filter?.phases ?? BOOTER_PHASES;
83
84 // Find booters registered to the BOOTERS_TAG by getting the bindings
85 const bindings = bootCtx.findByTag(BootTags.BOOTER);
86
87 // Prefix length. +1 because of `.` => 'booters.'
88 const prefixLength = BootBindings.BOOTERS.length + 1;
89
90 // Names of all registered booters.
91 const defaultBooterNames = bindings.map(binding =>
92 binding.key.slice(prefixLength),
93 );
94
95 // Determining the booters to be run. If a user set a booters filter (class
96 // names of booters that should be run), that is the value, otherwise it
97 // is all the registered booters by default.
98 const names = execOptions
99 ? execOptions.filter?.booters
100 ? execOptions.filter.booters
101 : defaultBooterNames
102 : defaultBooterNames;
103
104 // Filter bindings by names
105 const filteredBindings = bindings.filter(binding =>
106 names.includes(binding.key.slice(prefixLength)),
107 );
108
109 // Resolve Booter Instances
110 const booterInsts = await resolveList(filteredBindings, binding =>
111 // We cannot use Booter interface here because "filter.booters"
112 // allows arbitrary string values, not only the phases defined
113 // by Booter interface
114 bootCtx.get<{[phase: string]: () => Promise<void>}>(binding.key),
115 );
116
117 // Run phases of booters
118 for (const phase of phases) {
119 for (const inst of booterInsts) {
120 const instName = inst.constructor.name;
121 if (inst[phase]) {
122 debug(`${instName} phase: ${phase} starting.`);
123 await inst[phase]();
124 debug(`${instName} phase: ${phase} complete.`);
125 } else {
126 debug(`${instName} phase: ${phase} not implemented.`);
127 }
128 }
129 }
130
131 return bootCtx;
132 }
133}