@forrestjs/code
ForrestJS helps you splitting your App into small and reusable
components that can extend each other in a gentle and controlled fashion.
Yes, it is a simple plugin system inspired by the way Wordpress works.
*But traceable and fully debuggable.
Quick Code Example
import { registerAction, createExtension } from '@forrestjs/core';
registerAction('extendDoSmthCool', () => {
console.log('Inject even more cool stuff');
});
const doSmthCool = () => {
console.log('First cool thing');
createExtension('extendDoSmthCool');
console.log('Another cool thing');
};
doSmthCool();
If you run the code above, you'll get:
First cool thing
Inject even more cool stuff
Another cool thing
The cool part is that it supports both synchronous and asynchronous extension points,
and that an asynchronous extension point can run in both series or parallel.
Live Demoes on CodeSandbox
Install & Setup
npm add @forrestjs/core
There is no setup, just enjoy it!
Names of Things
An extension
is a controlled way to let plugins
inject some business logic.
An action
declare the intention to run a specific function
into an extension. It uses the Extensions's name as target
.
Create an Extension
[[OUTDATED - NEED UPDATE]]
Say you want to build an ExpressJS website. Exciting, right?
In the past you would have to list all your middlewares and routes in
the server configuration file: server.js
. I did that so many times!
But with hooks
things gets a little bit easier because you can simply let your
App open for extensions that come from modules you haven't yet thought of:
import express from 'express';
import { createHook } from '@forrestjs/core';
const app = express();
app.get('/', (req, res) => res.send('hello world'));
const registerRoute = (route, fn) => app.get(route, fn);
createHook.sync('express/routes', { registerRoute });
const port = createHook.waterfall('express/port', 8080).value;
app.listen(port);
๐ read also: createHook()
api
Register an Action
[[OUTDATED - NEED UPDATE]]
Now your new marketing department is asking you for a new landing page that should
answer to the /mighty-offer
route.
In the old days you would have needed to hack into server.js
and change it to add the
route. Oh well... With hooks
you can work this issue out without touching the rest of
your app:
import { registerAction } from '@forrestjs/core';
registerAction({
name: 'mightyOffer',
hook: 'express/routes',
handler: ({ registerRoute }) => {
registerRoute('/mighty-offer', (req, res) => {
res.send('oh boy, you should buy this and that and more...');
});
},
});
Then you simply need to import
your new extension before server.js
:
require('./mighty-offer');
require('./server');
That's it!
If you want to add an extension that changes the default port 8080
you should write
something like that in index.js
:
const customPort = () => 5050;
require('@forrestjs/core').registerAction('express/port', customPort);
๐ read also: registerAction()
api
How to know what is going on in an App like that?
[[OUTDATED - NEED UPDATE]]
The obvious drawback of this indirect code injection approach is that you could easily loose
control over the "what the hell is going on in my app".
What hooks into what?
Don't you worry, we have this covered. Add this to your index.js
:
const { traceHook } = require('@forrestjs/core');
console.log('Boot Trace:');
console.log('=================');
console.log(traceHook()('compact')('cli').join('\n'));
You should see something like that:
Boot Trace:
=================
mightyOffer ยป express/routes
customPort ยป express/port
I'm building a basic web app with an ExpressJS server, a GraphQL endpoint, a Postgres connection
manager plus few other services.
Here is what I see when I run the traceHook()
:
Boot Trace:
=================
โ env โ start
โ logger โ start
โ settings โ settings
โ hash โ init::services
โ jwt โ init::services
โ postgres โ init::services
โ express/graphql-test โ init::services
โ express โ init::services
โ express/cookie-helper โ express/middlewares
โ express/device-id โ express/middlewares
โ express/graphql โ express/routes
โ express/graphql-test โ express/graphql
โถ fii โ express/graphql-test
โถ fii โ express/graphql
โถ fii โ express/routes
โ express/ssr โ express/routes
โ postgres โ start::services
โ express โ start::services
โ boot โ finish
- You can read each line as
X hooks into Y
, where Y
is the hook name and X
is the extension name. - The vertical order is the sequence in which each hook is triggered.
- The indentation represents nested hooks.
(The little icons are part of the runHookApp()
utility that we cover in the next paragraph.=
I find this visualization quite simple to follow. But if you want an extensive reporting you should try:
const fullTrace = traceHook()('full')('json');
console.log(fullTrace);
๐ read also: traceHook()
api
Scaffold a Full Hooks App
[[OUTDATED - NEED UPDATE]]
This paragraph is going to cover a utility that provides a Hook based lifecycle for
developing a generic backend application. Long story short it helps you
packaging extension into reusable features.
Here is the examples we saw so far, packaged as a runHookApp()
:
const { runHookApp } = require('@forrestjs/core');
const { INIT_SERVICES, START_SERVICES } = require('@forrestjs/core');
const express = require('express');
const expressService = ({ registerAction }) => {
const name = 'express';
const app = express();
const registerRoute = (route, fn) => app.get(route, fn);
const registerMiddleware = (mountPoint, fn) => app.use(mountPoint, fn);
registerAction({
name,
hook: INIT_SERVICES,
handler: async ({ createHook, getConfig }) => {
await createHook.serie(expressService.EXPRESS_MIDDLEWARES, {
registerMiddleware,
});
await createHook.serie(expressService.EXPRESS_ROUTES, { registerRoute });
},
});
registerAction({
name,
hook: START_SERVICES,
handler: ({ getConfig }) => {
const port = getConfig('express.port', 8080);
app.listen(port, () => console.log(`Express listening on: ${port}`));
},
});
};
expressService.EXPRESS_MIDDLEWARES = `express/middlewares`;
expressService.EXPRESS_ROUTES = `express/routes`;
const homePageRoute = ({ registerRoute }) =>
registerRoute('/', (req, res) => {
res.send('Home!');
});
const mightyOfferFeature = ({ registerAction, getConfig }) => {
getConfig('mightyOffer.enabled') &&
registerAction({
name: 'mightyOffer',
hook: expressService.EXPRESS_ROUTES,
handler: ({ registerRoute }) => {
registerRoute('/offer', (req, res) => {
res.send(
`mighty offer... only for today ${getConfig(
'mightyOffer.price',
)}$!!!`,
);
});
},
});
};
runHookApp({
trace: 'compact',
settings: async ({ setConfig }) => {
setConfig('express.port', 5050);
setConfig('mightyOffer.enabled', true);
setConfig('mightyOffer.price', 5000);
},
services: [expressService],
features: [
[expressService.EXPRESS_ROUTES, homePageRoute],
mightyOfferFeature,
],
}).catch((err) => console.error(err));
I would normally split this code into multiple files:
express-service.js
home-route.js
(woule export the array we have in features
)mighty-offer.js
index.js
would run stuff and provide the configuration
With this setup a complex application can be takled by a large team with
people working in paralle on clearly separated features.
Reference Hooks by Name
[[OUTDATED - NEED UPDATE]]
A feature/service may register its hooks inside the hook app so that other
services/features may refer to them by name instead of importing constants:
const service1 = ({ registerHook, registerAction, createHook }) => {
registerHook({ S1_START: 's1' })
registerAction({
hook: '$START_SERVICE',
handler: () => createHook.sync('s1')
})
}
const feature1 = ['$S1_START', () => { ... }]
const feature2 = ['$S1_FOO?', () => { ... }]
runHookApp({
trace: 'compact',
services: [
service2,
],
features: [
feature1,
feature2,
],
}).catch(err => console.error(err))