Appearance
Patterns
Hitchy sticks to a conventions over configuration paradigm. There are several patterns described below that you should comply with to benefit from Hitchy's capabilities.
Code discovery
Usually, Javascript code relies on existing code which is imported or required.
javascript
import Path from "node:path";
export function getFile( folder ) {
return Path.resolve( folder, "foo.json" );
}
javascript
const Path = require( "node:path" );
exports.getFile = function( folder ) {
return Path.resolve( folder, "foo.json" );
};
In opposition to that, code files are discovered, imported and exposed automatically when starting a Hitchy-based application. In this
- discovery refers to Hitchy's search for plugins and components to integrate them with its runtime on start,
- importing refers to Hitchy's automatic loading of discovered plugins and components in proper order and
- exposure refers to Hitchy's API eventually covering APIs exported by discovered and imported plugins and components for use by other plugins and components or your application in general.
As a major benefit of this approach, Hitchy-based applications can be easily extended by plugins simply by installing them as a dependency of your application.
Configuration discovery
The same applies to a runtime's configuration which is read and compiled from several files.
Common Module Pattern
A Hitchy application primarily consists of plugins, components and their configuration. Either one is implemented as a Node.js module like this:
javascript
export function create() {
// add some code here
}
export function customFunction() {
// add some code here
}
javascript
exports.create = function() {
// add some code here
};
exports.customFunction = function() {
// add some code here
};
As an option, a default export can combine those properties:
javascript
export default {
create() {
// add some code here
},
customFunction() {
// add some code here
},
}
javascript
module.exports = {
create() {
// add some code here
},
customFunction() {
// add some code here
},
};
So far, these examples are common features of Node.js. Hitchy adds a feature on top of these default exports called common module pattern or CMP.
On discovering plugins, components and configurations, a default export of a factory function is accepted to deliver the module's actual API on invocation.
javascript
export default function( options ) {
const api = this;
return {
create() {
// add some code here
},
customFunction() {
// add some code here
},
};
}
javascript
module.exports = function( options ) {
const api = this;
return {
create() {
// add some code here
},
customFunction() {
// add some code here
},
};
};
In this example, the module is exposing the same API as before. But this time it is complying with the common module pattern e.g. to access Hitchy's API as illustrated in line two.
The exported factory function is invoked only once. Hitchy's API is provided as its this
. Global options describing the runtime's context and arguments provided on starting Hitchy are given in first argument. Based on the situation additional arguments may be given e.g. to expose an existing component the current one is about to replace for inheritance.
Complying with this pattern is beneficial in multiple ways:
Your modules are gaining access on runtime options such as CLI arguments provided on starting Hitchy or its API.
Because of that, a module could provide different implementations depending on current runtime environment.
Bootstrapping your application can be deferred by returning a promise for the module's API so you get all the time required to decide how to proceed.
Exporting classes
On exporting an ES6 class in a module Hitchy might falsely consider this module to comply with common module pattern for classes are represented by functions internally.
javascript
export default class MyServiceComponent {
// TODO add methods here
}
javascript
class MyServiceComponent {
// TODO add methods here
}
module.exports = MyServiceComponent;
This example will fail on Hitchy start with regard to invoking your exposed class without using operator new
. As a fix you might need to wrap this class in a function to actually comply with common module pattern.
javascript
export default function() {
class MyServiceComponent {
// TODO add methods here
}
return MyServiceComponent;
}
javascript
module.exports = function() {
class MyServiceComponent {
// TODO add methods here
}
return MyServiceComponent;
};
Alternatively you can add static property useCMP
set false
to prevent Hitchy from assuming this module is complying with common module pattern.
javascript
export default class MyServiceComponent {
// TODO add methods here
}
export const useCMP = false;
javascript
class MyServiceComponent {
// TODO add methods here
}
MyServiceComponent.useCMP = false;
module.exports = MyServiceComponent;
Returning Promise
The factory function of common module pattern may return a promise for the module's API instead of that API itself. This is deferring Hitchy's bootstrap process accordingly. On rejecting the promise, the process is failing causing Hitchy to exit on error.
javascript
export default function( options ) {
const api = this;
return someAsynchronousCode()
.then( () => {
return {
create() {
// add some code here
},
customFunction() {
// add some code here
},
};
} );
}
javascript
module.exports = function( options ) {
const api = this;
return someAsynchronousCode()
.then( () => {
return {
create() {
// add some code here
},
customFunction() {
// add some code here
},
};
} );
};
This feature is suitable for e.g. establishing connection with some service necessary for the application.
Passing Additional Information
Whenever Hitchy is supporting common module pattern it might intend to pass further information in addition to its API and options. This information will be provided as additional arguments following provided options.
Common Module Function Pattern
A similar pattern is named common module function pattern (CMFP). It is supported mostly for implementing Hitchy plugins e.g. when exposing a plugin's integration with Hitchy's plugins API.
Just like common module pattern it is meant to support provision of a function to be invoked for generating some data instead of providing that data immediately. Again, any such function is invoked with Hitchy's API provided as this
, Hitchy's options in first argument and any number of additional data provided in further arguments.
In opposition to common module pattern this one is not about a whole module's export to be generated dynamically but some property or method exported there.
Use Cases
When implementing a Hitchy-based application complying with common module pattern is superior over adopting common module function pattern. For example, when creating a service component most parts of Hitchy's API are available when loading the component in exposure stage. Thus sticking with common module pattern is suggested for implementing components like controllers and services as well as more dynamic configuration files.
However, when implementing a plugin for Hitchy instead of an application with Hitchy, that plugin's main file is exposing elements of plugins API to be retrieved at different stages of bootstrapping an application your plugin will be used with. In that case using common module function pattern is superior for either exported element is gaining access to a different amount of Hitchy's API whereas using common module pattern will have early access to a very limited API of Hitchy, only.
Let's consider this rather simple example of a plugin declaring blueprint routes:
javascript
export default {
blueprints: {
"/": "foo.index()",
"/bar": "foo.bar()",
},
}
javascript
module.exports = {
blueprints: {
"/": "foo.index()",
"/bar": "foo.bar()",
},
};
If you need to collect some information first to have a context-aware set of routes exported, you might want to stick with common module pattern:
javascript
export default function( options ) {
const api = this;
const blueprints = {};
// TODO collect routing definitions in blueprints variable
return {
blueprints,
};
}
javascript
module.exports = function( options ) {
const api = this;
const blueprints = {};
// TODO collect routing definitions in blueprints variable
return {
blueprints,
};
};
Using api
for dynamically creating values of exposed blueprints
property would fail most probably for this code is run at discovery stage of bootstrap when your plugin's module is loaded for the first time. At that point in time there is no access on eventually available plugins, there is no configuration and there are no components ready for use. It might even happen that your plugin will not make it into the application, finally.
According to plugins API, the blueprints
property is assumed to be a regular object or some map, by default. By adopting common module function pattern it becomes a function, though:
javascript
export default {
blueprints( options ) {
const api = this;
const blueprints = {};
// TODO collect routing definitions in blueprints variable
return {
blueprints,
};
},
}
javascript
module.exports = {
blueprints( options ) {
const api = this;
const blueprints = {};
// TODO collect routing definitions in blueprints variable
return {
blueprints,
};
},
};
Here, the module still gets loaded early in bootstrapping application. But routing definitions are not collected quite as early, but when Hitchy is in routing stage which is close to the end of bootstrapping with all plugins, models, services, controllers and configurations being exposed in api
.
On top of that, you may even return a promise for the eventually used routing definitions, again:
javascript
export default {
async blueprints( options ) {
const api = this;
const info = await someAsynchronousCode();
const blueprints = {};
// TODO collect routing definitions in blueprints variable
return {
blueprints,
};
},
}
javascript
module.exports = {
blueprints( options ) {
const api = this;
return someAsynchronousCode()
.then( info => {
const blueprints = {};
// TODO collect routing definitions in blueprints variable
return {
blueprints,
};
} );
},
};