Hi Doug,
This is Jean-Charles from PayPal (jasisk).
Just following up on #2732, itself related to #2633. Really appreciate your time on this. This is a long one so grab a sandwich or something. 😀
So as I described in the express issue, we use the ephemeral app pattern (my term) all over the place. The pattern is quite simple:
- In our module, we create and export a new app instance.
- In the user’s app, they can mount the returned app instance from step 1 just like any other middleware / app.
- In our module, we listen for
mount
. On mount we: grab the parent app instance, grab the mount path, and pop the app instance off the router stack.
The benefit here is our module now has a mountpath specified by the user and the parent app instance, while the user gets to use the familiar middleware registration pattern (app.use(module())
). You can see this pattern in use in pretty much all of our stuff (e.g., kraken-js, meddleware, swaggerize-express, express-enrouten, to name a few).
These problems come from meddleware. A quick intro on meddleware: it takes in a config, and then registers middleware. The short of it is that it enables configuration driven middleware registration. Useful when a thousand different departments write interdependent middleware since you can define arbitrary lifecycle ranges (e.g., auth should occur during priorities 60-70).
The first issue I wanted to attack is the one that 2633 was the result of: mountpath normalization. From the ephemeral app pattern, we get the mountpath the user supplied when adding our module (e.g., app.use('/this/path', kraken())
).
The meddleware config can take a route
property. In the simple case, this is essentially what it looks like:
config = {
"auth": {
"route": "/my-cool-path",
"module": "cool-custom-auth"
}
};
app.use(meddleware(config));
Which results in something like:
app.use('/my-cool-path', require('cool-custom-auth')());
Super simple.
The problem occurs with a slightly more complicated example:
config = {
"auth": {
"route": "/my-cool-path",
"module": "cool-custom-auth"
}
};
app.use('/secure', meddleware(config)); // addition of a mount path
… which would result in …
// mountpath is concatted with route property:
app.use('/secure/my-cool-path', require('cool-custom-auth')());
This works for strings, obviously, but breaks down if either mountpath
or route
are regexes or arrays containing regexes. You can see our naive approach implemented in the v3.x branch of meddleware.
My solution to this problem was to create a Router
, mount it to the parent app at mountpath
, and mount all the configured middleware on the router instance. This is implemented in the v4.x branch of meddleware. This solves the complex case described above:
config = {
"auth": {
"route": ["/my-cool-path", /.*secure.*/],
"module": "cool-custom-auth"
}
};
app.use('/new-path', meddleware(config));
… becomes …
var router = new express.Router();
router.use(["/my-cool-path", /.*secure.*/], require('cool-custom-auth')());
app.use('/new-path', router);
Great. Except one thing. We've broken this case:
config = {
"api": {
"route": "/api",
"module": "swaggerize-express" // uses the ephemeral app pattern
}
};
app.use('/my-app', meddleware(config));
… becomes …
var router = new express.Router();
router.use('/api', require('swaggerize-express')());
app.use('/my-app', router);
However /my-app
won't have anything on it because swaggerize-express
uses the ephemeral app pattern which assumes a mount
event. Since it's now being mounted on a Router
instead of an app
, there is no mount
event so it never initializes.
I can think of a way to solve this from meddleware with something like this (replacing the mounting line in meddleware):
fn = wrapFn(fn);
fn.parent = router;
router.use(spec.route || '/', fn);
fn.emit('mount', router);
While wrapFn
is a simple event emitter that replaces itself with fn
when it sees mount
, decides if it needs to refire on fn
based on some hueristic (listener count plus is app instance, perhaps).
So after that epic—any other ideas? 😀
Thanks again, Doug!
Ah, I see. I think I'm starting to understand what you're trying to accomplish :) Let me think on this a bit.