Skip to content

Commit 528d071

Browse files
committed
feat(runtime): add resolver, refactor container
The resolver and related refactor enables: 1. Lazy loading of app code, which is now the default. It doesn't preclude eager loading though, and future work could add an eager load option for production environments. 2. Customizable directory structures. This is generally not a good idea for the casual use case, but it's possible that some additional type of code asset might need different lookup rules (i.e. test files) 3. Cleaner addon / application loading. The application logger is now customizable by simply adding `app/logger.js` (as long as the interface matches). Same for the `app/router.js`. This also solves some outstanding issues: fixes #285, #284
1 parent 3b574c2 commit 528d071

File tree

12 files changed

+376
-331
lines changed

12 files changed

+376
-331
lines changed

app/logger.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '../lib/runtime/logger';

app/router.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '../lib/runtime/router';

lib/runtime/action.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import Instrumentation from '../metal/instrumentation';
2-
import Model from '../data/model';
3-
import Response from './response';
4-
import * as http from 'http';
5-
import * as createDebug from 'debug';
61
import {
72
assign,
83
capitalize,
@@ -12,6 +7,11 @@ import {
127
compact,
138
map
149
} from 'lodash';
10+
import Instrumentation from '../metal/instrumentation';
11+
import Model from '../data/model';
12+
import Response from './response';
13+
import * as http from 'http';
14+
import * as createDebug from 'debug';
1515
import * as assert from 'assert';
1616
import eachPrototype from '../metal/each-prototype';
1717
import DenaliObject from '../metal/object';
@@ -170,7 +170,7 @@ abstract class Action extends DenaliObject {
170170
this.request = options.request;
171171
this.logger = options.logger;
172172
this.container = options.container;
173-
this.config = this.container.config;
173+
this.config = this.container.lookup('app:main').config;
174174
}
175175

176176
/**

lib/runtime/addon.ts

+19-165
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
forEach,
3+
omit,
4+
noop
5+
} from 'lodash';
16
import * as path from 'path';
27
import * as fs from 'fs-extra';
38
import * as glob from 'glob';
@@ -7,18 +12,14 @@ import { sync as isDirectory } from 'is-directory';
712
import requireDir from '../utils/require-dir';
813
import * as tryRequire from 'try-require';
914
import * as stripExtension from 'strip-extension';
10-
import {
11-
forEach,
12-
omit,
13-
noop
14-
} from 'lodash';
1515
import { singularize } from 'inflection';
1616
import * as createDebug from 'debug';
1717
import DenaliObject from '../metal/object';
1818
import Container from './container';
1919
import Logger from './logger';
2020
import Router from './router';
2121
import Application from './application';
22+
import Resolver from './resolver';
2223

2324
const debug = createDebug('denali:runtime:addon');
2425

@@ -32,7 +33,6 @@ export interface AddonOptions {
3233
environment: string;
3334
dir: string;
3435
container: Container;
35-
logger: Logger;
3636
pkg?: any;
3737
}
3838

@@ -85,57 +85,36 @@ export default class Addon extends DenaliObject {
8585
public dir: string;
8686

8787
/**
88-
* The list of child addons that this addon contains
89-
*/
90-
public addons: Addon[];
91-
92-
/**
93-
* The application logger instance
88+
* The package.json for this addon
9489
*
9590
* @since 0.1.0
9691
*/
97-
protected logger: Logger;
92+
public pkg: any;
9893

9994
/**
100-
* The package.json for this addon
95+
* The resolver instance to use with this addon.
96+
*
97+
* @since 0.1.0
10198
*/
102-
public pkg: any;
99+
public resolver: Resolver;
103100

104101
/**
105-
* Internal cache of the configuration that is specific to this addon
102+
* The consuming application container instance
103+
*
104+
* @since 0.1.0
106105
*/
107-
public _config: any;
106+
public container: Container;
108107

109108
constructor(options: AddonOptions) {
110109
super();
111110
this.environment = options.environment;
112111
this.dir = options.dir;
112+
this.pkg = options.pkg || tryRequire(findup('package.json', { cwd: this.dir }));
113113
this.container = options.container;
114-
this.logger = options.logger;
115114

116-
this.pkg = options.pkg || tryRequire(findup('package.json', { cwd: this.dir }));
115+
this.resolver = this.resolver || new Resolver(this.dir);
116+
this.container.addResolver(this.resolver);
117117
this.container.register(`addon:${ this.pkg.name }@${ this.pkg.version }`, this);
118-
this._config = this.loadConfig();
119-
}
120-
121-
/**
122-
* The app directory for this addon. Override to customize where the app directory is stored in
123-
* your addon.
124-
*
125-
* @since 0.1.0
126-
*/
127-
get appDir(): string {
128-
return path.join(this.dir, 'app');
129-
}
130-
131-
/**
132-
* The config directory for this addon. Override this to customize where the config files are
133-
* stored in your addon.
134-
*
135-
* @since 0.1.0
136-
*/
137-
public get configDir(): string {
138-
return path.join(this.dir, 'config');
139118
}
140119

141120
/**
@@ -148,131 +127,6 @@ export default class Addon extends DenaliObject {
148127
return (this.pkg && this.pkg.name) || 'anonymous-addon';
149128
}
150129

151-
/**
152-
* Load the config for this addon. The standard `config/environment.js` file is loaded by default.
153-
* `config/middleware.js` and `config/routes.js` are ignored. All other userland config files are
154-
* loaded into the container under their filenames.
155-
*
156-
* Config files are all .js files, so just the exported functions are loaded here. The functions
157-
* are run later, during application initialization, to generate the actual runtime configuration.
158-
*/
159-
protected loadConfig(): any {
160-
let config = this.loadConfigFile('environment') || function() {
161-
return {};
162-
};
163-
if (isDirectory(this.configDir)) {
164-
let allConfigFiles = requireDir(this.configDir, { recurse: false });
165-
let extraConfigFiles = omit(allConfigFiles, 'environment', 'middleware', 'routes');
166-
forEach(extraConfigFiles, (configModule, configFilename) => {
167-
let configModulename = stripExtension(configFilename);
168-
this.container.register(`config:${ configModulename }`, configModule);
169-
});
170-
}
171-
return config;
172-
}
173-
174-
/**
175-
* Load the addon's various assets. Loads child addons first, meaning that addon loading is
176-
* depth-first recursive.
177-
*/
178-
public load(): void {
179-
debug(`loading ${ this.pkg.name }`);
180-
this.loadInitializers();
181-
this.loadMiddleware();
182-
this.loadApp();
183-
this.loadRoutes();
184-
}
185-
186-
/**
187-
* Load the initializers for this addon. Initializers live in `config/initializers`.
188-
*/
189-
protected loadInitializers(): void {
190-
let initializersDir = path.join(this.configDir, 'initializers');
191-
if (isDirectory(initializersDir)) {
192-
let initializers = requireDir(initializersDir);
193-
forEach(initializers, (initializer, name) => {
194-
this.container.register(`initializer:${ name }`, initializer);
195-
});
196-
}
197-
}
198-
199-
/**
200-
* Load the middleware for this addon. Middleware is specified in `config/middleware.js`. The file
201-
* should export a function that accepts the router as it's single argument. You can then attach
202-
* any middleware you'd like to that router, and it will execute before any route handling by
203-
* Denali.
204-
*
205-
* Typically this is useful to register global middleware, i.e. a CORS handler, cookie parser,
206-
* etc.
207-
*
208-
* If you want to run some logic before certain routes only, try using filters on your actions
209-
* instead.
210-
*/
211-
protected loadMiddleware(): void {
212-
this._middleware = this.loadConfigFile('middleware') || noop;
213-
}
214-
215-
/**
216-
* The middleware factory for this addon.
217-
*/
218-
public _middleware: (router: Router, application: Application) => void;
219-
220-
/**
221-
* Loads the routes for this addon. Routes are defined in `config/routes.js`. The file should
222-
* export a function that defines routes. See the Routing guide for details on how to define
223-
* routes.
224-
*/
225-
protected loadRoutes(): void {
226-
this._routes = this.loadConfigFile('routes') || noop;
227-
}
228-
229-
/**
230-
* The routes factory for this addon.
231-
*/
232-
public _routes: (router: Router) => void;
233-
234-
/**
235-
* Load the app assets for this addon. These are the various classes that live under `app/`,
236-
* including actions, models, etc., as well as any custom class types.
237-
*
238-
* Files are loaded into the container under their folder's namespace, so `app/roles/admin.js`
239-
* would be registered as 'role:admin' in the container. Deeply nested folders become part of the
240-
* module name, i.e. `app/roles/employees/manager.js` becomes 'role:employees/manager'.
241-
*
242-
* Non-JS files are loaded as well, and their container names include the extension, so
243-
* `app/mailer/welcome.html` becomes `mail:welcome.html`.
244-
*/
245-
protected loadApp(): void {
246-
debug(`loading app for ${ this.pkg.name }`);
247-
if (fs.existsSync(this.appDir)) {
248-
eachDir(this.appDir, (dirname) => {
249-
debug(`loading ${ dirname } for ${ this.pkg.name }`);
250-
let dir = path.join(this.appDir, dirname);
251-
let type = singularize(dirname);
252-
253-
glob.sync('**/*', { cwd: dir }).forEach((filepath) => {
254-
let modulepath = stripExtension(filepath);
255-
if (filepath.endsWith('.js')) {
256-
let Class = require(path.join(dir, filepath));
257-
Class = Class.default || Class;
258-
this.container.register(`${ type }:${ modulepath }`, Class);
259-
} else if (filepath.endsWith('.json')) {
260-
let mod = require(path.join(dir, filepath));
261-
this.container.register(`${ type }:${ modulepath }`, mod.default || mod);
262-
}
263-
});
264-
});
265-
}
266-
}
267-
268-
/**
269-
* Helper to load a file from the config directory
270-
*/
271-
protected loadConfigFile(filename: string): any {
272-
let configModule = tryRequire(path.join(this.configDir, `${ filename }.js`));
273-
return configModule && (configModule.default || configModule);
274-
}
275-
276130
/**
277131
* A hook to perform any shutdown actions necessary to gracefully exit the application, i.e. close
278132
* database/socket connections.

0 commit comments

Comments
 (0)