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 |
|
6 | import {
|
7 | Application,
|
8 | Context,
|
9 | CoreBindings,
|
10 | inject,
|
11 | resolveList,
|
12 | } from '@loopback/core';
|
13 | import debugModule from 'debug';
|
14 | import {resolve} from 'path';
|
15 | import {BootBindings, BootTags} from './keys';
|
16 | import {bindBooter} from './mixins';
|
17 | import {
|
18 | Bootable,
|
19 | BOOTER_PHASES,
|
20 | BootExecutionOptions,
|
21 | BootOptions,
|
22 | } from './types';
|
23 |
|
24 | const 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 | */
|
37 | export class Bootstrapper {
|
38 | constructor(
|
39 | (CoreBindings.APPLICATION_INSTANCE)
|
40 | private app: Application & Bootable,
|
41 | private projectRoot: string,
(BootBindings.PROJECT_ROOT) |
42 | true})
(BootBindings.BOOT_OPTIONS, {optional: |
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 | }
|