Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/apostrophecms/moog-require
Leverage npm's "require" with the "moog" module subclassing tool
https://github.com/apostrophecms/moog-require
Last synced: about 2 months ago
JSON representation
Leverage npm's "require" with the "moog" module subclassing tool
- Host: GitHub
- URL: https://github.com/apostrophecms/moog-require
- Owner: apostrophecms
- Created: 2015-01-20T19:00:48.000Z (almost 10 years ago)
- Default Branch: main
- Last Pushed: 2021-09-15T20:02:41.000Z (over 3 years ago)
- Last Synced: 2024-10-04T11:45:37.714Z (3 months ago)
- Language: JavaScript
- Homepage:
- Size: 93.8 KB
- Stars: 3
- Watchers: 12
- Forks: 2
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
[![Build Status](https://travis-ci.org/punkave/moog-require.svg?branch=master)](https://travis-ci.org/punkave/moog-require)
# moog-require
`moog-require` provides powerful module subclassing for server-side development. It extends the features of [moog](https://github.com/punkave/moog) with the following additions:
* Fetches modules from a local modules folder if they are not defined explicitly
* If a module is defined explicitly and also exists in localModules, the local modules folder becomes a source of defaults for properties not defined explicitly
* Fetches modules from npm if they are not defined either explicitly or via the local modules folder
* If a module exists by the same name both in npm and via explicit definition or local modules, automatically extends the npm module without the need for a new name (like the "category" feature of Objective C)
* Provides access to an "asset chain" of subclass module directories and type names, to implement template overrides and the like
* Also supports bundling moog modules in a single npm module, if explicitly configured## Example
```javascript
// IMPLICIT BASE CLASS OF ALL MODULES
// (if configured - see app.js below)
//
// In node_modules/module/index.jsmodule.exports = {
self.construct = function(self, options) {
self.renderTemplate = function(name, data) {
var i;
for (i = 0; (i < options.__meta.length); i++) {
var meta = options.__meta[i];
var path = meta.dirname + '/views/' + name + '.html';
if (fs.existsSync(path)) {
// Deepest subclass wins
return templateEngine.render(path, data);
}
}
};};
};// NPM MODULE
//
// In node_modules/events/index.jsmodule.exports = {
color: 'red',construct: function(self, options) {
self.defaultTags = options.tags;
self.get = function(params, callback) {
// Go get some events
return callback(events);
};
}
}// PROJECT LEVEL SUBCLASS OF NPM MODULE
//
// in lib/modules/events/index.js
module.exports = {
color: 'green',construct: function(self, options) {
var superGet = self.get;
self.get = function(params, callback) {
// override: only interested in upcoming events
params.upcoming = true;
return superGet(params, callback);
};
}
};// in app.js
var synth = require('moog-require')({
localModules: __dirname + '/lib/modules',
defaultBaseClass: 'module'
});synth.define({
// SETTING A DEFAULT OPTION THAT APPLIES TO *ALL* MODULES
// (because we're setting it for the defaultBaseClass)
'module': {
color: 'gray'
},// CONFIGURATION (IMPLICIT SUBCLASS) OF A PROJECT-LEVEL MODULE
// (same technique works to configure an npm module)'events': {
color: 'blue',
// More overrides in lib/modules/events/index.js (above).
// Anything here in app.js wins
},// EXTENDING A PROJECT-LEVEL MODULE TO CREATE A NEW ONE
'parties': {// Let's subclass a module right here in app.js (usually we'd just
// set site-specific options here and put code in
// lib/modules/parties/index.js, but you're not restricted)extend: 'events',
color: 'lavender',// Let's alter the "tags" option before the
// base class constructors are aware of itbeforeConstruct: function(self, options) {
options.tags = (options.tags || []).concat('party');
},// This constructor can take a callback, even though
// the base classes don't. You can mix and matchconstruct: function(self, options, callback) {
// options.color will be lavender
var superGet = self.get;
self.get = function(params, callback) {
// override: only interested in parties. Let's
// assume the base class uses this as a query
params.title = /party/i;
return superGet(params, callback);
};// Output names and full folder paths of all modules in the
// subclassing chain; we can use this to push assets and
// implement template overrides
console.log(options._directories);
},setBridge: function(modules) {
// Do something that requires access to the
// other modules, which are properties of
// the modules object
}
}
});// Instantiate all the modules, passing in some
// universal options that are provided to all of them. This
// only instantiates modules mentioned in `definitions`, but
// they may override or subclass modules in npm or the
// project-level modules folderreturn synth.createAll({ mailer: myMailer }, function(err, modules) {
return modules.events.get({ ... }, function(err, events) {
...
});
});// We can also tell the modules about each other. This
// invokes the setBridge method of each module, if any,
// and passes the modules object to itsynth.bridge(modules);
// We can also create an instance of any module at any time,
// and pass it additional options. This is useful if you are
// not following the singleton pattern. We don't promise
// killer performance if you create thousands of objects
// per secondreturn synth.create('parties', { color: 'purple' }, function(err, party) {
...
});
```## Replacing a module with another npm module
The `monsters` npm module works great for most people, but you've created a superior replacement, `scary-monsters`. And you want people to be able to use it as a drop-in replacement, without changing code that refers to the `monsters` module.
This is especially useful if you want other moog types that subclass `monsters` to automatically subclass `scary-monsters` instead.
So the `index.js` of your `scary-monsters` npm module might look like:
```javascript
module.exports = {
replace: 'monsters',
construct: function(self, options) { ... }
}
```Note the `replace` property.
Now, an application developer who wants to use `scary-monsters` instead of the usual `monsters` module can simply configure it instead of `monsters`. The type name `monsters` will still be defined. The type name `scary-monsters` is **not** defined.
```javascript
var synth = require('moog-require')({
localModules: __dirname + '/lib/modules',
defaultBaseClass: 'module'
});synth.define({
'scary-monsters': { ... configuration ... }
});// This works
synth.create('monsters', {});// This does NOT work
synth.create('scary-monsters', {});
```Note that if you want to further extend `scary-monsters` at project level, you should use a `lib/modules/scary-monsters` folder. Anything in `lib/modules/monsters` will be ignored. Similarly, in `app.js`, don't configure `monsters`, just configure `scary-monsters`.
## Improving a module with another npm module: implicit subclassing
The `improve` property is similar to `replace`, but allows you to implicitly subclass an existing type rather than completely replacing it. If you `improve` the `monsters` type, all other code will regard your subclass as the `monsters` type.
This is useful if you wish to release an npm module that subclasses a well-known module to add more functionality, without requiring developers to change the source code of other modules in order to use it.
Here is an example:
So the `index.js` of your `scary-monsters` npm module might look like:
```javascript
module.exports = {
improve: 'monsters',
construct: function(self, options) {
var superJump = self.jump;
self.jump = function(howHigh) {
// Limit height of jumps
if (howHigh > 100) {
howHigh = 100;
}
// Call original version
superJump(howHigh / 2);
};
}
}
```Note the `improve` property.
Just like the `replace` option, the `improve` option defines the type with the name specified by `improve`. That is, your subclass is substituted everywhere for the `monsters` type. The `scary-monsters` type is **not** defined.
Here is an example of application-level code:
```javascript
var synth = require('moog-require')({
localModules: __dirname + '/lib/modules',
defaultBaseClass: 'module'
});synth.define({
'scary-monsters': { ... configuration ... }
});// This works
synth.create('monsters', {});// This does NOT work
synth.create('scary-monsters', {});
```Note that if you want to further extend `scary-monsters` at project level, you should use a `lib/modules/scary-monsters` folder. Code in `lib/modules/monsters` will be loaded, but it will subclass the original `monsters` module, and then `scary-monsters` will subclass that. This is probably not what you want. Similarly, in `app.js`, don't configure `monsters`, just configure `scary-monsters`.
## Calling `require` yourself
Don't.
Well, okay...
If you want to write this:
```javascript
`extend': require('./lib/weird-place/my-module/index.js')
```You may do so, but in that case your module must export its `__name`, `__dirname` and `__filename`, like so:
```javascript
module.exports = {
__name: 'my-module',
__dirname: __dirname,
__filename: __filename,
construct: function(self, options) { ... }
};
```This is only necessary if you are using `require` directly. Most of the time, you will be happier if you just specify a module name and let us `require` it for you. This even works in npm modules. (Yes, it will still find it if it is an npm dependency of your own module.)
## Packaging multiple moog-require modules in a single npm module
Sometimes several modules are conceptually distinct, but are developed and versioned in tandem. In these cases there is no benefit from separate packaging, just a significant delay in `npm install`. npm peer dependencies are one way to handle this, but [npm peer dependencies may be on the chopping block](http://dailyjs.com/2014/04/16/node-roundup/), and they are significantly slower than pre-packaging modules together.
The difficulty of course is that the link between npm module names and moog-require module names is broken when we do this. So we need another way to indicate to moog-require that it should look in the appropriate place.
Since searching for "X", where X is actually provided by module "Y", is not a core feature of npm itself we have kept this mechanism simple: you can give `moog-require` an array of npm module names that contain a "bundle" of definitions rather than a single definition. An npm "bundle" module then must export a `moogBundle` array property which contains the names of the moog-require modules it defines. The actual definitions live in `lib/modules/module-one/index.js`, `lib/modules/module-two/index.js`, etc. *within the bundle npm module*. `moog-require` will find these automatically and will consider these first before requiring normally from npm.
Here's an example:
```javascript
// In node_modules/mybundle/index.jsmodule.exports = {
moogBundle: {
modules: [ 'module-one', 'module-two' ],
directory: 'lib/modules'
}
};// In node_modules/mybundle/lib/modules/module-one/index.js
module.exports = {
construct: function(self, options) { ... }
};// In node_modules/mybundle/lib/modules/module-two/index.js
module.exports = {
construct: function(self, options) { ... }
};
``````javascript
// In our applicationvar synth = require('moog-require')({
bundles: [ 'mybundle' ],
localModules: __dirname + '/lib/modules',
defaultBaseClass: 'module'
});synth.define({
'module-one': {},
'module-two': {}
});
```Note that just as before, we must include these modules in our explicit `define` calls if we want to instantiate them with `createAll`, although we don't have to override any properties; we can pass empty objects to just use the defaults defined in the project level folder, and/or implicitly inherit from npm.
However, you may explicitly `create` a type that exists only in the project level folder and/or npm.
## Nesting modules in subdirectories
For your convenience, `moog-require` has optional support for loading modules from nested subdirectories. The rules of the game are very simple:
* You must set the `nestedModuleSubdirs` option to `true`.
* Modules can now be found nested beneath your `localModules` folder, at any depth.
* The names of the parent directories **do not matter**. They are purely for your organizational convenience.
* The name of the actual module directory must still be the full name of the module.If the same module exists in two places, an exception is thrown.
## Changelog
**The 2.x series is deprecated for new work, as its functionality was folded into Apostrophe 3.x. See below for 1.x release notes relevant to maintenance of Apostrophe 2.x.**
1.3.2: starting in version 1.3.1, this module only loads other modules via `npm` if they are explicit npm dependencies, which is necessary for stability and security. However, it is too strict: if the project has no `package.json` at all at the level of `app.js`, `npm` search up the tree, and this module should too. Beginning in verison 1.3.2, it does search up the tree. However it stops at the first `package.json` found.
1.3.1: `moog-require` loads modules from npm if they exist there and are configured by name in the application. This was always intended only as a way to load direct, intentional dependencies of your project. However, since npm "flattens" the dependency tree, dependencies of dependencies that happen to have the same name as a project-level module could be loaded by default, crashing the site or causing unexpected behavior. So beginning with this release, `moog-require` scans `package.json` to verify an npm module is actually a dependency of the project itself before attempting to load it.
1.3.0: achieved an approximately 100x performance improvement when `nestedModuleSubdirs` is in use by fetching
a list of index.js files on the first `define` call and then searching that prefetched list each
time. This solution is much faster than the glob module cache.1.2.0: use `originalToMy` to handle moog class names with npm namespaces in them.
1.1.1: use `importFresh` to avoid bugs when two instances of `moog-require` are loading the same module definitions. Previously if modules created by the two instances later modified sub-properties of `options`, they would inadvertently share values. This fix is critical for both `apostrophe-monitor` and `apostrophe-multisite`.
1.1.0: support for the `nestedModuleSubdirs` option.
1.0.1: shallowly clone the result of `require` rather than attaching `.__meta` to a potentially shared object. This allows multiple instances of `moog-require` in multiple instances of `apostrophe` to independently track where modules were loaded from.
1.0.0: `moog`, `async` and `lodash` dependencies updated to satisfy `npm audit`. Declared 1.x as this has been a stable part of Apostrophe 2.x for a long time.
0.4.1: fixed `moog` dependency to use the version that supports `autoload: false`.
0.4.0: added `isImprovement` method which returns true if a type name turned out to be an improvement of another type via the `improve` keyword. This is useful when you wish to instantiate all of the types except for those that are just improvements of others.
0.3.0: introduced the `replace` and `improve` options, which allow an npm module to substitute itself for another moog type completely, or enhance it via implicit subclassing. This is useful when releasing a drop-in replacement for a well-known module.
0.2.0: depends on `moog` 0.2.0 which introduces the `mirror` method.
0.1.0: compatible with `moog` 0.1.0 in which the `__meta` property became an object with `chain` and `name` properties.