1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const path = require ('path');
|
18 | const {ensureDir} = require ('fs-extra');
|
19 | const debug = require ('debug')('blueprint:app');
|
20 | const assert = require ('assert');
|
21 | const { forOwn, get, isArray, mapValues } = require ('lodash');
|
22 |
|
23 | const lookup = require ('./-lookup');
|
24 | const { BO, computed } = require ('base-object');
|
25 | const BPromise = require ('bluebird');
|
26 |
|
27 | const ApplicationModule = require ('./application-module');
|
28 | const ModuleLoader = require ('./module-loader');
|
29 | const RouterBuilder = require ('./router-builder');
|
30 | const Server = require ('./server');
|
31 | const Loader = require ('./loader');
|
32 |
|
33 | const { Events } = require ('./messaging');
|
34 |
|
35 | const DEFAULT_APPLICATION_NAME = '<unnamed>';
|
36 | const APPLICATION_MODULE_NAME = '$';
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | module.exports = BO.extend (Events, {
|
44 |
|
45 | started: false,
|
46 |
|
47 |
|
48 | _appModule: null,
|
49 |
|
50 |
|
51 | _defaultLoader: new Loader (),
|
52 |
|
53 |
|
54 | tempPath: computed ({
|
55 | configurable: true,
|
56 | get () { return path.resolve (this.appPath, '.blueprint'); }
|
57 | }),
|
58 |
|
59 | viewsPath: computed ({
|
60 | get () { return this._appModule.viewsPath; }
|
61 | }),
|
62 |
|
63 |
|
64 | resources: computed ({
|
65 | get () { return this._appModule.resources; }
|
66 | }),
|
67 |
|
68 |
|
69 | server: computed ({
|
70 | get () { return this._server; }
|
71 | }),
|
72 |
|
73 | module: computed ({
|
74 | get () { return this._appModule; }
|
75 | }),
|
76 |
|
77 | init () {
|
78 | this._super.call (this, ...arguments);
|
79 | this._modules = {};
|
80 |
|
81 |
|
82 |
|
83 | this._appModule = new ApplicationModule ({
|
84 | name: APPLICATION_MODULE_NAME,
|
85 | app: this,
|
86 | modulePath: this.appPath
|
87 | });
|
88 |
|
89 | this._server = new Server ({app: this});
|
90 | },
|
91 |
|
92 | |
93 |
|
94 |
|
95 | configure () {
|
96 | return ensureDir (this.tempPath)
|
97 | .then (() => this._loadConfigurationFiles ())
|
98 | .then (configs => {
|
99 |
|
100 | this.configs = configs;
|
101 | this.name = get (configs, 'app.name', DEFAULT_APPLICATION_NAME);
|
102 |
|
103 | // Load all the modules for the application that appear in the node_modules
|
104 | // directory. We consider these the auto-loaded modules for the application.
|
105 | // We handle these before the modules that are explicitly loaded by the application.
|
106 |
|
107 | let moduleLoader = new ModuleLoader ({app: this});
|
108 | moduleLoader.on ('loading', module => this._modules[module.name] = module);
|
109 |
|
110 | return moduleLoader.load ();
|
111 | })
|
112 |
|
113 |
|
114 | .then (() => this._appModule.configure ())
|
115 | .then (() => {
|
116 |
|
117 | let {services} = this.resources;
|
118 |
|
119 | return BPromise.props (mapValues (services, (service, name) => {
|
120 | debug (`configuring service ${name}`);
|
121 |
|
122 | return service.configure ();
|
123 | }));
|
124 | })
|
125 | .then (() => this._server.configure (this.configs.server))
|
126 |
|
127 |
|
128 |
|
129 | .then (() => this._appModule.hasViews ? this._server.importViews (this._appModule.viewsPath) : null)
|
130 | .then (() => {
|
131 | const builder = new RouterBuilder ({app: this});
|
132 | const {routers} = this.resources;
|
133 |
|
134 | return builder.addRouter ('/', routers).build ();
|
135 | })
|
136 | .then (router => {
|
137 |
|
138 | this._server.setMainRouter (router);
|
139 | return this.emit ('blueprint.app.initialized', this);
|
140 | })
|
141 | .then (() => this);
|
142 | },
|
143 |
|
144 | |
145 |
|
146 |
|
147 | destroy () {
|
148 | return this._server.close ().then (() => {
|
149 |
|
150 | let { services } = this.resources;
|
151 |
|
152 | return BPromise.props (mapValues (services, (service, name) => {
|
153 | debug (`destroying service ${name}`);
|
154 | return service.destroy ();
|
155 | }));
|
156 | });
|
157 | },
|
158 |
|
159 | |
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | addModule (name, appModule) {
|
166 | if (this._modules.hasOwnProperty (name))
|
167 | throw new Error (`duplicate module ${name}`);
|
168 |
|
169 | return this._importViewsFromModule (appModule)
|
170 | .then (() => { this._appModule.merge (appModule) })
|
171 | .then (() => {
|
172 | this._modules[name] = appModule;
|
173 | return this;
|
174 | });
|
175 | },
|
176 |
|
177 | _importViewsFromModule (appModule) {
|
178 | if (!appModule.hasViews)
|
179 | return Promise.resolve ();
|
180 |
|
181 | return this._server.importViews (appModule.viewsPath);
|
182 | },
|
183 |
|
184 | |
185 |
|
186 |
|
187 |
|
188 | start () {
|
189 |
|
190 |
|
191 | this.emit ('blueprint.app.starting', this);
|
192 |
|
193 |
|
194 | let {services} = this.resources;
|
195 | let promises = mapValues (services, (service, name) => {
|
196 | debug (`starting service ${name}`);
|
197 |
|
198 | return service.start ();
|
199 | });
|
200 |
|
201 | return BPromise.props (promises)
|
202 | .then (() => this._server.listen ())
|
203 | .then (() => {
|
204 |
|
205 | this.started = true;
|
206 | return this.emit ('blueprint.app.started', this);
|
207 | });
|
208 | },
|
209 |
|
210 | |
211 |
|
212 |
|
213 | restart () {
|
214 | return this.emit ('blueprint.app.restart', this);
|
215 | },
|
216 |
|
217 | |
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 | lookup (component) {
|
236 |
|
237 |
|
238 |
|
239 |
|
240 | if (isArray (component)) {
|
241 | if (component[0] === 'config')
|
242 | return get (this.configs, component.slice (1));
|
243 | else
|
244 | return lookup (this.resources, component);
|
245 | }
|
246 | else if (component.startsWith ('config:')) {
|
247 |
|
248 |
|
249 | const name = component.slice (7);
|
250 | return get (this.configs, name);
|
251 | }
|
252 | else {
|
253 |
|
254 |
|
255 |
|
256 |
|
257 | const parts = component.split (':');
|
258 |
|
259 | if (parts.length === 2) {
|
260 | return lookup (this.resources, component);
|
261 | }
|
262 | else if (parts.length === 3) {
|
263 |
|
264 | const targetModule = this._modules[parts[1]];
|
265 | assert (!!targetModule, `The module named ${targetModule} does not exist.`);
|
266 |
|
267 |
|
268 |
|
269 |
|
270 | const name = `${parts[0]}:${parts[2]}`;
|
271 | return targetModule.lookup (name);
|
272 | }
|
273 | else {
|
274 | throw new Error ('The component name is invalid.');
|
275 | }
|
276 | }
|
277 | },
|
278 |
|
279 | |
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 | asset (filename, opts) {
|
288 | return this._appModule.asset (filename, opts);
|
289 | },
|
290 |
|
291 | assetSync (filename, opts) {
|
292 | return this._appModule.assetSync (filename, opts);
|
293 | },
|
294 |
|
295 | |
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 | mount (routerName) {
|
302 | debug (`mounting router ${routerName}`);
|
303 |
|
304 | const router = this.lookup (`router:${routerName}`);
|
305 | assert (!!router, `The router {${routerName}} does not exist.`);
|
306 |
|
307 | const builder = new RouterBuilder ({app: this});
|
308 | return builder.addRouter ('/', router).build ();
|
309 | },
|
310 |
|
311 | |
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 | _loadConfigurationFiles () {
|
319 | const dirname = path.resolve (this.appPath, 'configs');
|
320 | return this._defaultLoader.load ({dirname});
|
321 | }
|
322 | });
|