A Starflow plugin provides classes that perform a specific action that is somehow linked to a step. The most basic form of a plugin looks like this:
// index.js, i.e. the plugin entry point
module.exports = function (starflow) {
return {
factories: {
foo: require('./lib/foo')(starflow)
}
};
};
// lib/foo.js
module.exports = function (starflow) {
function Foo() {
// 'my-plugin.foo' is the name of the executable
starflow.BaseExecutable.call(this, 'my-plugin.foo');
}
Foo.prototype = Object.create(starflow.BaseExecutable.prototype);
Foo.prototype.constructor = Foo;
Foo.prototype.exec = function exec() {
starflow.logger.log('Hello, World!');
};
// always export a factory
return function () {
return new Foo();
};
};
The executable classes you write are used when Starflow processes the steps written by an end-user.
Essentially, a step is transformed into a task that holds a reference to an executable instance.
When the task is processed, it calls the exec
method of its executable.
The steps transformation and tasks execution mechanics are handled by Starflow, leaving you with writing the executable definitions.
A Starflow plugin is basically a function that:
- takes a reference to
starflow
as its first parameter - exports an object which contains the
factories
property that holds a map to the different executable factories provided by the plugin
An executable object must extend its prototype from the one from starflow.BaseExecutable
. In object-oriented programming terms, an executable class extends starflow.BaseExecutable
.
Let's write a plugin that can create a new file based on a handlebars template. For example, based on the template ngService.hbs
, I want to create a new Angular 1.X service with the name bob
and the dependencies $q
and $window
.
The main file is the one required by an user when calling addPlugin
on its workflow.
Make sure the following entry is present in the package.json
:
{
"main": "index.js"
}
Then create the index.js
file at the root of the project with:
// index.js
module.exports = function tplProcessorPlugin(starflow) {
return {
factories: {
ngService: require('./lib/ngService')(starflow)
}
};
};
As you can see, the executable factories are stored in the ./lib
directory of the project.
// templates/ngService.hbs
angular
.module('{{moduleName}}')
.service('{{name}}Service', [{{#each dependencies}}'{{this}}', {{/each}}function ({{#each dependencies}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}) {
function {{capitalizedName}}Service() {
}
return {{capitalizedName}}Service;
}]);
// lib/ngService.js
module.exports = function (starflow) {
var fs = require('fs');
var path = require('path');
var Handlebars = require('handlebars');
function NgService() {
starflow.BaseExecutable.call(this, 'tplProcessor.ngService');
}
NgService.prototype = Object.create(starflow.BaseExecutable.prototype);
NgService.prototype.constructor = NgService;
NgService.prototype.exec = function exec(moduleName, name, dependencies, location) {
var source = fs.readFileSync(path.resolve(__dirname, '../templates/ngService.hbs'), 'utf8');
var template = Handlebars.compile(source);
var context = {
moduleName: moduleName,
name: name,
capitalizedName: name.charAt(0).toUpperCase() + name.slice(1),
dependencies: dependencies
};
var target = template(context);
fs.writeFileSync(path.join(location, name + 'Service.js'), target);
};
return function () {
return new NgService();
};
};
Finally, you can use this plugin like this:
// createUserService.js
var starflow = require('starlfow');
var steps = [
{'tplProcessor.ngService': [
'my.module',
'user',
['$q', '$window'],
'path/for/createdFile'
]}
];
var workflow = new starflow.Workflow(steps);
return workflow
.addPlugin(require('starflow-tpl-processor'))
.run();
What if the executables of your plugin need to use a web service? You'll probably have to provide some credentials and base URL. Starflow uses the rc package to hold some config values and credentials.
For instance, the starflow-github plugin has a GithubService that uses starflow.config.get('github.TOKEN')
to access the API.
These config values are stored somewhere on the user's machine, for example in its environmental variables or in a .starflowrc
file at the root of its project.
We recommend storing sensible information as env variables whereas the rest (e.g. shell.SPAWN_DEPTH_LIMIT
) can be stored in a .starflowrc
file.
Let's pretend the templates are stored somewhere in the cloud (e.g. The Templates Store that exposes an API) and not in the plugin "templates" directory anymore. We'll need some credentials to authenticate the user in order to get the template.
First, let's create a .starflowrc
file at the root of our project:
{
"tplProcessor": {
"URL": "https://templates-store.com"
}
}
Then export the following env variables (e.g. from your ~/.bash_profile
):
export starflow_tplProcessor__USERNAME=bob
export starflow_tplProcessor__PASSWORD=abc123
Note: it's important to start the env variable names with
starflow_
or else they will be ignored. Please check the rc project for more details.
Now, we're going to create a templatesStoreService that will communicate with the API from The Templates Store. Furthermore, we'll have to change some parts of our ngService executable to use templatesStoreService.
// templatesStoreService.js (e.g. located in the lib directory of the plugin)
module.exports = function (starflow) {
// npm i --save request bluebird
var request = require('request');
var Promise = require('bluebird');
var url = starflow.config.get('tplProcessor.URL');
if (!url) {
throw new Error('TemplatesStore url is required');
}
var username = starflow.config.get('tplProcessor.USERNAME');
var password = starflow.config.get('tplProcessor.PASSWORD');
var token = new Buffer(username + ':' + password);
var templatesStoreService = {
baseUrl: url,
headers: {
'Authorization': 'Basic ' + token.toString('base64')
},
getTemplate: function getTemplate(name) {
return new Promise(function (resolve, reject) {
request({url: this.baseUrl + '/templates/' + name, headers: this.headers}, function (error, response, body) {
if (!error && response.statusCode === 200) {
resolve(body);
} else {
// do something to prepare the error message
reject('There was an error');
}
});
}.bind(this));
}
};
return templatesStoreService;
};
Let's adapt the executable so it gets the template from the web service and not the local "templates" directory anymore.
// lib/ngService.js
module.exports = function (starflow) {
// get the templates store service we created
var templatesStoreService = require('./templatesStoreService')(starflow);
var fs = require('fs');
var path = require('path');
var Handlebars = require('handlebars');
function NgService() {
starflow.BaseExecutable.call(this, 'tplProcessor.ngService');
}
NgService.prototype = Object.create(starflow.BaseExecutable.prototype);
NgService.prototype.constructor = NgService;
NgService.prototype.exec = function exec(moduleName, name, dependencies, location) {
// get the template from a web service instead of a local file
return templatesStoreService
.getTemplate(name)
.then(function (response) {
var template = Handlebars.compile(response);
var context = {
moduleName: moduleName,
name: name,
capitalizedName: name.charAt(0).toUpperCase() + name.slice(1),
dependencies: dependencies
};
var target = template(context);
fs.writeFileSync(path.join(location, name + 'Service.js'), target);
});
};
return function () {
return new NgService();
};
};
And that's it!
If you want people to use your plugin, you have to provide a clear and understandable documentation about it.
If someone wants to use your plugin, these are basically the 2 pieces of information he will need:
- Does your plugin require configuration (such as credentials)?
- What are the executables (most important: their name and the args they need) that are provided and what do they do?
By providing answers to these questions, users won't have to check your source code to know what your plugin actually does. You can check the starflow-shell README to get ideas on how to write yours.