Source: Plugin.js

  1. 'use strict'
  2. /**
  3. * Dependencies
  4. * @ignore
  5. */
  6. const _ = require('lodash')
  7. const path = require('path')
  8. const injector = require('./injectorInstance')
  9. const callsite = require('callsite')
  10. /**
  11. * Symbols
  12. * @ignore
  13. */
  14. let init = Symbol()
  15. /**
  16. * Plugin
  17. *
  18. * @class
  19. * Instances of Plugin expose an API which can be accessed by the developer to
  20. * define plugins, along with methods used by the Registry to manage plugin lifecycle.
  21. *
  22. * The developer API can be used to:
  23. *
  24. * - register node modules as dependencies on the injector
  25. * - include files in the plugin definition for managing large plugins
  26. * - define dependencies using factory methods
  27. * - alias dependencies
  28. * - create adapters to determine which implementation of an interface to
  29. * use at runtime
  30. * - access dependencies for mutation without creating new dependencies
  31. * - extend the plugin API
  32. * - register callbacks for managing plugin lifecycle
  33. *
  34. * @example <caption>Metadata</caption>
  35. * {
  36. * version: '0.0.1',
  37. * dependencies: {
  38. * '<PLUGIN NAME>': '>=1.2.3'
  39. * }
  40. * }
  41. *
  42. */
  43. class Plugin {
  44. /**
  45. * Constructor
  46. *
  47. * @description
  48. * Initialize a new Plugin instance.
  49. *
  50. * @param {string} name - The name of the plugin
  51. * @param {Object} metadata - The plugin metadata
  52. *
  53. */
  54. constructor (name, metadata) {
  55. this.name = name
  56. this.metadata = metadata
  57. }
  58. /**
  59. * Require
  60. *
  61. * @description
  62. * Adds dependencies to the injector by loading node modules. Accepts a string,
  63. * array or object. By passing an object you can alias the package name.
  64. *
  65. * @param {(string|Array|Object)} modules - Modules to require
  66. * @returns {this}
  67. *
  68. * @example <caption>Usage</caption>
  69. *
  70. * app.plugin(<NAME>, <METADATA>)
  71. * .initializer(function (plugin) {
  72. * plugin.require('express')
  73. *
  74. * plugin.require([
  75. * 'crypto',
  76. * 'ioredis'
  77. * ])
  78. *
  79. * plugin.require({
  80. * '_': 'lodash',
  81. * 'fs': 'fs-extra',
  82. * 'myLibrary': './myLibrary'
  83. * })
  84. * })
  85. */
  86. require (modules) {
  87. if (typeof modules === 'string') {
  88. let name = modules
  89. modules = {}
  90. modules[name] = name
  91. }
  92. if (Array.isArray(modules)) {
  93. modules = _.zipObject(modules, modules)
  94. }
  95. Object.keys(modules).forEach(key => {
  96. this.module(key, modules[key])
  97. })
  98. return this
  99. }
  100. /**
  101. * Include
  102. *
  103. * @description
  104. * The bootstrapping process searches through the plugins directory looking for
  105. * plugins to load. To make a new plugin you simply create a sub-directory with an
  106. * index.js file. Additional files in the directory are incorporated using the
  107. * include method. This allows developers to separate out one plugin into an
  108. * arbitrary number of files.
  109. *
  110. * @param {string} filename - Relative path to included file.
  111. * @returns {this}
  112. *
  113. * @example <caption>Usage</caption>
  114. *
  115. * // index.js
  116. * 'use strict'
  117. *
  118. * module.exports = function (sunstone) {
  119. * app.plugin('MyResource', {
  120. * version: '0.0.1',
  121. * dependencies: {
  122. * 'Server': '0.0.1'
  123. * }
  124. * })
  125. * .initializer(function (plugin) {
  126. * plugin
  127. * .include('./other')
  128. * .include('./yetanother')
  129. * })
  130. * }
  131. *
  132. * // other.js
  133. * 'use strict'
  134. *
  135. * module.exports = function (plugin) {
  136. *
  137. * plugin
  138. * .factory('MyModel', function (a, b) {
  139. * // ...
  140. * })
  141. * .router('MyModelRouter', function (MyModel) {
  142. * // ...
  143. * })
  144. *
  145. * }
  146. */
  147. include (filename) {
  148. // prepend file path from call stack onto the given, possibly relative, filename
  149. let caller = callsite()[1]
  150. let callerpath = caller.getFileName()
  151. let filepath = path.join(path.dirname(callerpath), filename)
  152. require(filepath)(this)
  153. return this
  154. }
  155. /**
  156. * Module
  157. *
  158. * @description
  159. * This is used internally by `plugin.require()` to register modules with
  160. * the type "module". This is important to maintain a distinction between
  161. * components provided by plugins defined in the host (or extending applications)
  162. * and components that originate from node modules.
  163. *
  164. * @param {string} name - Dependency name
  165. * @param {string} node_module - Name of external node dependency
  166. * @private
  167. */
  168. module (name, node_module) {
  169. injector.register({
  170. name,
  171. type: 'module',
  172. plugin: this.name,
  173. fn: function () {
  174. return require(node_module)
  175. }
  176. })
  177. }
  178. /**
  179. * Factory
  180. *
  181. * @description
  182. * The factory method registers a new dependency on the injector, validates it, and
  183. * determines which other dependencies it requires.
  184. *
  185. * The first argument is the name of the new dependency and the second argument is a
  186. * function that returns the value of the dependency. However, this dependency is not
  187. * invoked at the time the dependency is registered.
  188. *
  189. * Getting a dependency from the Injector invokes the function and stores the return
  190. * value.
  191. *
  192. * @param {string} name - Dependency name
  193. * @param {function} fn - Factory function
  194. * @returns {this}
  195. *
  196. * @example <caption>Usage</caption>
  197. *
  198. * plugin
  199. * .factory('one', function () {
  200. * return 1
  201. * })
  202. * .factory('two', function () {
  203. * return 2
  204. * })
  205. * .factory('oneplustwo', function (one, two) {
  206. * return one + two
  207. * })
  208. *
  209. */
  210. factory (name, fn) {
  211. injector.register({
  212. name,
  213. type: 'factory',
  214. plugin: this.name,
  215. fn
  216. })
  217. return this
  218. }
  219. /**
  220. * Adapter
  221. *
  222. * @description
  223. * Create factories that determine which implementation
  224. * to use at injection time.
  225. *
  226. * @param {string} name - Dependency name
  227. * @param {function} fn - Factory function
  228. * @returns {this}
  229. *
  230. * @example <caption>Usage</caption>
  231. *
  232. * plugin
  233. * .factory('RedisResource', function () {})
  234. * .factory('MongoResource', function () {})
  235. * .adapter('Resource', function (injector, settings) {
  236. * // where settings.property is 'RedisResource' or 'MongoResource'
  237. * return injector.get(settings.property)
  238. * })
  239. * .factory('User', function (Resource) {})
  240. *
  241. */
  242. adapter (name, fn) {
  243. injector.register({
  244. name,
  245. type: 'adapter',
  246. plugin: this.name,
  247. fn
  248. })
  249. return this
  250. }
  251. /**
  252. * Alias
  253. *
  254. * @description
  255. * Alias creates a reference to another item on the injector
  256. * within its own injector object. When the alias is injected
  257. * through the use of the injector.get() method it calls the
  258. * aliased dependency and creates a reference to the instance
  259. * in the dependency.value field.
  260. *
  261. * @param {string} alias - New dependency name
  262. * @param {string} name - Existing dependency name
  263. * @returns {this}
  264. *
  265. * @example <caption>Usage</caption>
  266. *
  267. * plugin
  268. * .factory('myDependency', function () {
  269. * // ...
  270. * })
  271. * .alias('myAlias', 'myDependency')
  272. */
  273. alias (alias, name) {
  274. injector.register({
  275. name: alias,
  276. type: 'alias',
  277. plugin: this.name,
  278. fn: () => {
  279. return injector.get(name)
  280. }
  281. })
  282. return this
  283. }
  284. /**
  285. * Extension
  286. *
  287. * @description
  288. * The idea of an extension is that you can access some component
  289. * to use it's API without registering anything on the injector.
  290. *
  291. * This is useful for things like modifying a model's schema or
  292. * registering event handlers on an event emitter.
  293. *
  294. * @param {string} name - Dependency name
  295. * @param {function} fn - Mutator function
  296. * @returns {this}
  297. *
  298. * @example <caption>Extending Data Schema</caption>
  299. *
  300. * // Given a plugin created as follows
  301. * app.plugin('Default API', <METADATA>)
  302. * .initializer(function (plugin) {
  303. * .factory('User', function (Resource) {
  304. * class User extends Resource {
  305. * static get schema () {
  306. * return Object.assign({}, super.schema, {
  307. * name: { type: 'string' },
  308. * email: { type: 'string', format: 'email' }
  309. * })
  310. * }
  311. * }
  312. *
  313. * return User
  314. * })
  315. * })
  316. *
  317. * app.plugin('My Project', <METADATA>)
  318. * .initializer(function (plugin) {
  319. * plugin.extension('UserExtension', function (User) {
  320. * User.extendSchema({
  321. * domainSpecificAttribute: { type: 'whatever', ... }
  322. * })
  323. * })
  324. * })
  325. *
  326. * @example <caption>Adding Event Handler</caption>
  327. *
  328. * app.plugin(<NAME>, <METADATA>)
  329. * .initializer(function (plugin) {
  330. * plugin.factory('emitter', function () {
  331. * return new EventEmitter()
  332. * })
  333. * })
  334. *
  335. * app.plugin('My Project', <METADATA>)
  336. * .initializer(function (plugin) {
  337. * plugin.extension('CustomEventHandlers', function (emitter) {
  338. * emitter.on('ready', function (event) {
  339. * // do something
  340. * })
  341. * })
  342. * })
  343. */
  344. extension (name, fn) {
  345. injector.register({
  346. name,
  347. type: 'extension',
  348. plugin: this.name,
  349. fn
  350. })
  351. return this
  352. }
  353. /**
  354. * Assembler
  355. *
  356. * @param {string} name - Dependency name
  357. * @param {function} fn - Assembler function
  358. * @returns {this}
  359. *
  360. * @description
  361. * This can be used to define new types of components. For example, the core
  362. * framework probably doesn't need any knowledge of Express routers, but if you
  363. * wanted to define a specialized factory registrar for routers, you could do it
  364. * like so:
  365. *
  366. * ```js
  367. * app.plugin('server', {
  368. * version: '0.0.0'
  369. * })
  370. * .initializer(function (plugin) {
  371. * plugin.assembler('router', function (injector) {
  372. * let plugin = this
  373. * return function (name, factory) {
  374. * injector.register({
  375. * name,
  376. * type: 'router',
  377. * plugin: plugin.name,
  378. * factory
  379. * })
  380. * })
  381. * })
  382. * })
  383. * ```
  384. *
  385. * This makes a new dependency registrar called 'router' that can be used as follows:
  386. *
  387. * ```js
  388. * app.plugin('other', {
  389. * version: '0.0.0'
  390. * })
  391. * .initializer(function (plugin) {
  392. * plugin.router('SomeRouter', function (Router, SomeResource) {
  393. * let router = new Router()
  394. *
  395. * router.get('endpoint', function () {
  396. * SomeResource
  397. * .list(req.query)
  398. * .then(function (results) {
  399. * res.json(results)
  400. * })
  401. * .catch(error => next(error))
  402. * })
  403. *
  404. * return router
  405. * })
  406. * })
  407. * ```
  408. *
  409. * The dependency inject can then be queried by this new "type" value.
  410. *
  411. * ```js
  412. * injector.filter({ type: 'router' })
  413. * ```
  414. *
  415. * @todo there should possibly be a way to create a starter method automatically for an assembler to save that boilerplate
  416. */
  417. assembler (name, fn) {
  418. this.constructor.prototype[name] = fn(injector, this)
  419. return this
  420. }
  421. /**
  422. * Lifecycle Management
  423. *
  424. * These methods are used to register lifecycle methods that will be called
  425. * by the plugin manager
  426. */
  427. /**
  428. * Initializer
  429. *
  430. * @description
  431. * Register an initializer function.
  432. *
  433. * @param {callback} callback - Initializer function
  434. * @returns {this}
  435. *
  436. * @example <caption>Usage</caption>
  437. *
  438. * module.exports = function (app) {
  439. * app.plugin('MyResource', {
  440. * version: '0.0.1',
  441. * dependencies: {
  442. * 'Server': '0.0.1'
  443. * }
  444. * })
  445. * .initializer(function (plugin) {
  446. * .include('./other')
  447. * .include('./yetanother')
  448. * })
  449. * }
  450. *
  451. */
  452. initializer (callback) {
  453. this[init] = callback
  454. return this
  455. }
  456. /**
  457. * Initialize
  458. *
  459. * @description
  460. * Invoke the initializer function, if it exists.
  461. *
  462. * @returns {this}
  463. */
  464. initialize () {
  465. let fn = this[init]
  466. if (fn) {
  467. fn(this)
  468. }
  469. return this
  470. }
  471. /**
  472. * Starter
  473. *
  474. * @description
  475. * Register an starter function.
  476. *
  477. * @param {function} callback - Starter function
  478. * @returns {this}
  479. *
  480. * @example <caption>Usage</caption>
  481. *
  482. * app.plugin(<NAME>, <METADATA>)
  483. * .initializer(function (plugin) {
  484. * plugin.starter(function (injector, server) {
  485. * injector
  486. * .filter({ plugin: this.name, type: 'router' })
  487. * .values()
  488. * .forEach(router => {
  489. * router.mount(server)
  490. * })
  491. * })
  492. * })
  493. *
  494. */
  495. starter (fn) {
  496. injector.register({
  497. name: `${this.name}:starter`,
  498. type: 'callback',
  499. plugin: this.name,
  500. fn
  501. })
  502. return this
  503. }
  504. /**
  505. * Start
  506. *
  507. * @description
  508. * Invoke the starter function, if it exists.
  509. *
  510. * @returns {this}
  511. */
  512. start () {
  513. injector.invoke(`${this.name}:starter`)
  514. return this
  515. }
  516. /**
  517. * Stopper
  518. *
  519. * @description
  520. * Register an initializer function.
  521. *
  522. * @param {function} callback - Stopper function
  523. * @returns {this}
  524. *
  525. * @example <caption>Usage</caption>
  526. *
  527. * app.plugin(<NAME>, <METADATA>)
  528. * .initializer(function (plugin) {
  529. * plugin.stopper(function (injector, server) {
  530. * // code to disable plugin
  531. * })
  532. * })
  533. *
  534. */
  535. stopper (fn) {
  536. injector.register({
  537. name: `${this.name}:stopper`,
  538. type: 'callback',
  539. plugin: this.name,
  540. fn
  541. })
  542. return this
  543. }
  544. /**
  545. * Stop
  546. *
  547. * @description
  548. * Invoke the stopper function, if it exists.
  549. *
  550. * @returns {this}
  551. */
  552. stop () {
  553. injector.invoke(`${this.name}:stopper`)
  554. return this
  555. }
  556. }
  557. /**
  558. * Exports
  559. */
  560. module.exports = Plugin