1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. 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 {Constructor} from '@loopback/core';
|
7 | import debugFactory from 'debug';
|
8 | import path from 'path';
|
9 | import {ArtifactOptions, Booter} from '../types';
|
10 | import {discoverFiles, loadClassesFromFiles} from './booter-utils';
|
11 |
|
12 | const debug = debugFactory('loopback:boot:base-artifact-booter');
|
13 |
|
14 | /**
|
15 | * This class serves as a base class for Booters which follow a pattern of
|
16 | * configure, discover files in a folder(s) using explicit folder / extensions
|
17 | * or a glob pattern and lastly identifying exported classes from such files and
|
18 | * performing an action on such files such as binding them.
|
19 | *
|
20 | * Any Booter extending this base class is expected to
|
21 | *
|
22 | * 1. Set the 'options' property to a object of ArtifactOptions type. (Each extending
|
23 | * class should provide defaults for the ArtifactOptions and use Object.assign to merge
|
24 | * the properties with user provided Options).
|
25 | * 2. Provide it's own logic for 'load' after calling 'await super.load()' to
|
26 | * actually boot the Artifact classes.
|
27 | *
|
28 | * Currently supports the following boot phases: configure, discover, load.
|
29 | *
|
30 | */
|
31 | export class BaseArtifactBooter implements Booter {
|
32 | /**
|
33 | * Options being used by the Booter.
|
34 | */
|
35 | readonly options: ArtifactOptions;
|
36 | /**
|
37 | * Project root relative to which all other paths are resolved
|
38 | */
|
39 | readonly projectRoot: string;
|
40 | /**
|
41 | * Relative paths of directories to be searched
|
42 | */
|
43 | dirs: string[];
|
44 | /**
|
45 | * File extensions to be searched
|
46 | */
|
47 | extensions: string[];
|
48 | /**
|
49 | * `glob` pattern to match artifact paths
|
50 | */
|
51 | glob: string;
|
52 |
|
53 | /**
|
54 | * List of files discovered by the Booter that matched artifact requirements
|
55 | */
|
56 | discovered: string[];
|
57 | /**
|
58 | * List of exported classes discovered in the files
|
59 | */
|
60 | classes: Constructor<{}>[];
|
61 |
|
62 | constructor(projectRoot: string, options: ArtifactOptions) {
|
63 | this.projectRoot = projectRoot;
|
64 | this.options = options;
|
65 | }
|
66 |
|
67 | /**
|
68 | * Get the name of the artifact loaded by this booter, e.g. "Controller".
|
69 | * Subclasses can override the default logic based on the class name.
|
70 | */
|
71 | get artifactName(): string {
|
72 | return this.constructor.name.replace(/Booter$/, '');
|
73 | }
|
74 |
|
75 | /**
|
76 | * Configure the Booter by initializing the 'dirs', 'extensions' and 'glob'
|
77 | * properties.
|
78 | *
|
79 | * NOTE: All properties are configured even if all aren't used.
|
80 | */
|
81 | async configure() {
|
82 | this.dirs = this.options.dirs
|
83 | ? Array.isArray(this.options.dirs)
|
84 | ? this.options.dirs
|
85 | : [this.options.dirs]
|
86 | : [];
|
87 |
|
88 | this.extensions = this.options.extensions
|
89 | ? Array.isArray(this.options.extensions)
|
90 | ? this.options.extensions
|
91 | : [this.options.extensions]
|
92 | : [];
|
93 |
|
94 | let joinedDirs = this.dirs.join(',');
|
95 | if (this.dirs.length > 1) joinedDirs = `{${joinedDirs}}`;
|
96 |
|
97 | const joinedExts = `@(${this.extensions.join('|')})`;
|
98 |
|
99 | this.glob = this.options.glob
|
100 | ? this.options.glob
|
101 | : `/${joinedDirs}/${this.options.nested ? '**/*' : '*'}${joinedExts}`;
|
102 | }
|
103 |
|
104 | /**
|
105 | * Discover files based on the 'glob' property relative to the 'projectRoot'.
|
106 | * Discovered artifact files matching the pattern are saved to the
|
107 | * 'discovered' property.
|
108 | */
|
109 | async discover() {
|
110 | debug(
|
111 | 'Discovering %s artifacts in %j using glob %j',
|
112 | this.artifactName,
|
113 | this.projectRoot,
|
114 | this.glob,
|
115 | );
|
116 |
|
117 | this.discovered = await discoverFiles(this.glob, this.projectRoot);
|
118 |
|
119 | if (debug.enabled) {
|
120 | debug(
|
121 | 'Artifact files found: %s',
|
122 | JSON.stringify(
|
123 | this.discovered.map(f => path.relative(this.projectRoot, f)),
|
124 | null,
|
125 | 2,
|
126 | ),
|
127 | );
|
128 | }
|
129 | }
|
130 |
|
131 | /**
|
132 | * Filters the exports of 'discovered' files to only be Classes (in case
|
133 | * function / types are exported) as an artifact is a Class. The filtered
|
134 | * artifact Classes are saved in the 'classes' property.
|
135 | *
|
136 | * NOTE: Booters extending this class should call this method (await super.load())
|
137 | * and then process the artifact classes as appropriate.
|
138 | */
|
139 | async load() {
|
140 | this.classes = loadClassesFromFiles(this.discovered, this.projectRoot);
|
141 | }
|
142 | }
|