Building Your Own AdonisJS Package

in utopian-io •  7 years ago  (edited)

Building Your Own Adonis Package

Repository

https://github.com/adonisjs

Keeping code reusable has always been a key tenet of great software. Thinking in a modular fashion can help us maximize benefits derived from previously existing code. As modern developers, we can create or access packages cutting across multiple languages and frameworks. These packages are usually concerned with solving a specific task and we can leverage them to speed up our workflow.

Today, we'll be building our very own Adonis package. In the course of building this package, we'll be introduced to various concepts. We will work with the CLI (Command Line Interface) to enable us greatly speed up our ease of access to certain resources.

Difficulty

  • Intermediate

What Will I Learn?

At the end of this tutorial, we hope to have covered enough concepts to enable us build solid packages for NPM. We'll cover the following

  • Creating Service Providers for Adonis Packages.
  • Leveraging Lifecycle Hooks Within Adonis During Installation.
  • Auto-generating and Copying Files From the CLI.
  • Providing Configurable Data Objects for End Users

Requirements

Introduction

Previously, I wrote a tutorial on TDD with AdonisJS that is worth a quick read. On that tutorial, we built an API app called Homely that keeps an inventory of apartments available for users.

In this tutorial however, we'll be building an AdonisJS package called Sophos. This is a simple package that returns quotes about startups when issued a CLI command. It's a very basic building block but it can serve as a foundation for more adventures in the future.

Before we commence, we must first understand the role of service providers in building AdonisJS packages.

Service Providers in Adonis Packages.

Before we can start leveraging service providers, we must take a brief detour and examine them.

What are Service Providers?

The official Adonis docs states service providers as simple ES6 classes with lifecycle methods that are used to register and bootstrap bindings.

Adonis packages need their service providers to be able to provide the end user with any appreciable benefits. In AdonisJS, all service provider classes must extend the @adonisjs/fold super class. Service provider classes expose two lifecyle methods: register and boot public methods.

  1. The register method is used to register any bindings, and it's a best practice to never use another binding inside this method.

  2. The boot method is called only when all providers have been registered. If you need to access previously existing bindings, you do it in this method.

A typical service provider class looks like the one below:

const { ServiceProvider } = require('@adonisjs/fold')

class SophosProvider extends ServiceProvider {
  register () {
    // register bindings
  }

  boot () {
    // optionally do some intial setup
  }
}

module.exports = SophosProvider

Setting Up the Sophos Package

We're assuming your development machine runs the Linux operating system. Windows users will be right at home here also. We're also assuming you have the Node runtime, NPM and Git installed.

Let's create an NPM package at your preferred directory. We can do that by running:

C:\Users\hp\Documents>mkdir sophos && cd sophos
C:\Users\hp\Documents\sophos>npm init -y    

Package Generation Shot

We now have a basic NPM package with a package.json ready for our use.

Let's add some info to our package.json. Mine looks like this:

{
  "name": "sophos",
  "version": "1.0.0",
  "description": "Bite-size nuggets of inspiration for your next big startup idea.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["adonisjs", "node"],
  "author": "Caleb Mathew <[email protected]>",
  "license": "ISC"
}

We'll add a couple of dependencies.

  "devDependencies": {
    "@adonisjs/ace": "^5.0.1",
    "@adonisjs/fold": "^4.0.7",
   "@adonisjs/framework": "^5.0.6",
    "japa": "^1.0.6",
    "japa-cli": "^1.0.1",
},

"dependencies": {
    "request": 2.81.0            
}

Setting Up Package Structure

We'll need to setup a structure for our package. We'll need a structure like the one below

  • config/
    • index.js
  • providers/
    • SophosProvider.js
  • src/
    • Sophos.js
  • instructions.js
  • instructions.md
  • package.json
  • README.md

Let's properly understand each of these files. We'll examine them on a micro level:

  • config/index.js: We'll store any configuration options we'll like the user of our package to overwrite here.

  • providers/SophosProvider.js: This is the single service provider responsible for registering our package bindings.

  • src/Sophos.js: Our core package class. Provides the core functionality for our app.

  • instructions.js: This is a module that runs whenever an AdonisJS package has been installed successfully.

  • instructions.md: This is a markdown file that is processed into HTML and displays on the browser whenever an AdonisJS package is successfully installed.

  • README.md: Information that is of some relevance to the end user.

Getting To Work

We'll get to work setting up the service provider for our package. Our service provider class will register a couple of bindings. Let's open up the providers/SophosProvider.js and add some code.

'use strict'

/**
 * adonis-sophos
 *
 * (c) Caleb Mathew <[email protected]>
 *
 */

const { ServiceProvider } = require('@adonisjs/fold')

class SophosProvider extends ServiceProvider {
  register () {
    this.app.singleton('Adonis/Addons/Sophos', (app) => {
      const Config = app.use('Adonis/Src/Config')
      const Sophos = require('../src/Sophos')

      return new Sophos(Config)
    })

    this.app.alias('Adonis/Addons/Sophos', 'Sophos')
  }
}

module.exports = SophosProvider

As previously explained, every service provider must extend the @adonisjs/fold superclass. In our register method, we create a singleton with the namespace Adonis/Addons/Sophos. A singleton is a binding that occurs once and is resolved continually for the remainder of an applications lifecycle. The corresponding closure method accepts a parameter, app that is basically an instance of the IOC (Inversion of Control) container.

We inject the Config class from the AdonisJS IOC container and we also require our Sophos class. We then return an instantiation of the Sophos class with the Config IOC injection as a parameter.

All that's left is the aliasing of the Adonis/Addons/Sophos namespace to the shorter, more attractive Sophos namespace and finally, exporting the module for use elsewhere.

Next, we get to work on our Sophos base class.

'use strict'

/**
 * adonis-sophos
 *
 * (c) Caleb Mathew <[email protected]>
 *
 */

 const request = require('request');

 /**
  * The Sophos class makes a request to a URL returning a promise
  * resolved with data or rejected with an error.
  *
  * @class Sophos
  *
  * @param {Object} Config
  */
 class Sophos {
    sourceURL = 'https://wisdomapi.herokuapp.com/v1/';

    constructor (Config) {
     this.config = Config.merge('sophos', {
       sourceURL: this.sourceURL
     })
    }

    getQuotes(id = null) {
        let suffix = id ? `${id}` : `random`
        let endpoint = `${this.config.sourceURL}/${suffix}`

        return new Promise((resolve, reject) => {
            request(endpoint, { json: true }, (err, res, body) => {
              if (err) return reject(err);
              return resolve(body)
            });
        });
    }

}

We are requiring the request NPM module as a dependency we'll need for making a HTTP request to the API endpoint. In our Sophos class, we define a few defaults like the sourceURL we'll be making the requests to and which can be overriden by user defined configuration. We use the Config.merge method to allow users override our defaults.

Next, we create the getQuotes method that makes the request to our sourceURL and returns a promise that is resolved if we are successful and rejected otherwise.

We just need to add some configuration defaults now. Open up config/index.js and add some code. We are only adding a single default (sourceURL).

'use strict'

/*
|--------------------------------------------------------------------------
| Sophos
|--------------------------------------------------------------------------
|
| Sophos returns bits of inspiration for your next big startup idea.
|
*/

module.exports = {
  /*
  |--------------------------------------------------------------------------
  | Source URL
  |--------------------------------------------------------------------------
  |
  | The URL of the resource queried for data.
  |
  */
  sourceURL: 'https://wisdomapi.herokuapp.com/v1/',
}

Our package is nearly complete. We simply need to specify what happens when our package is installed. Let's work on the instructions.js module.

'use strict'

const path = require('path')

module.exports = async (cli) => {
  try {
    const inFile = path.join(__dirname, './config', 'index.js')
    const outFile = path.join(cli.helpers.configPath(), 'sophos.js')
    await cli.copy(inFile, outFile)
    cli.command.completed('create', 'config/sophos.js')
  } catch (error) {
    // ignore error
  }
}

Whenever our package is successfully installed, we copy our default config file over to the main config folder. We also make sure we rename the config file from index.js to sophos.js before copying and we do all these through the CLI.

Finally, we just have to provide some installation instructions for our users and we're done.

## Register provider
Register provider inside `start/app.js` file.

```js
const providers = [
  '@adonisjs/sophos/providers/SophosProvider'
]

And then you can access it as follows

const Sophos = use('Sophos')

That's all folks! To get a random quote, we just have to follow the above instructions and call it like this in our code.

```javascript
    const Sophos = use('Sophos')
    
    Sophos.getQuote().then(quote => {
        res.json(quote)
    })

Using Sophos From the CLI

Let's use Sophos to display a message from the cli. Create a directory called commands and in Quote.js, we'll add some code.

'use strict'

const { Command } = use('@adonisjs/ace')
const Sophos = use('Sophos')

class Quote extends Command {
  static get signature () {
    return 'quote'
  }

  static get description () {
    return 'Startup juice and motivation.'
  }

  async handle (args, options) {
      Sophos.getQuotes(quote => {
          console.log(`${this.chalk.gray(quote.author.name)} - ${this.chalk.cyan(quote.author.company)}`)
          console.log(`${quote.content}`)
      })
  }
}

module.exports = Quote

Next, we need to add some bindings for the CLI to function properly. We'll add this binding to the register method in the SophosProvider class.

    this.app.bind('Adonis/Commands/Quote', () => require('../commands/Quote'))

Finally, we need to add some code to the boot method.

  boot () {
      const ace = require('@adonisjs/ace')

      ace.addCommand('Adonis/Commands/Quote')
  }

We can test our code by copying our package structure to an existing Adonisjs project and registering its service provider.

Next, heading into the terminal and running adonis quote should give us a screen like below:

Screen after Request

Conclusion

We've covered a few key concepts in our tutorial today. We've learned about the power of service providers, setting up configuration options in an Adonis package and copying files through the CLI. We also ran some installation hooks for our package.

Resources

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  
  ·  7 years ago Reveal Comment

Thank you for the nice work.
While many of the content was already adopted from the official docs, which you referenced (thank you for that), I did find some added value and a new perspective for building Adonis packages.
My advise is to try to cover areas not as closely referenceable by the official documentation, and provide more variety to your readers.
Looking forward for more work from your side.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Noted.

Will explore more areas in subsequent posts.

Hey @creatrixity

We're already looking forward to your next contribution!

Contributing on Utopian

Learn how to contribute on our website or by watching this tutorial on Youtube.

Utopian Witness!

Vote for Utopian Witness! We are made of developers, system administrators, entrepreneurs, artists, content creators, thinkers. We embrace every nationality, mindset and belief.

Want to chat? Join us on Discord https://discord.gg/h52nFrV

WARNING - The message you received from @pukhtoon is a CONFIRMED SCAM!
DO NOT FOLLOW any instruction and DO NOT CLICK on any link in the comment!

For more information, read this post:
https://steemit.com/steemit/@arcange/virus-infection-threat-reported-searchingmagnified-dot-com

If you find my work to protect you and the community valuable, please consider to upvote this warning or to vote for my witness.