Plugin is nothing but an independent feature that is generic in nature and can be hooked to an application with minimum effort and configuration. A frontend plugin could be a calendar, slider, typeahead or anything that enhances the functionality of currently available features in the application. It could be a big application on its own like a photo gallery with a zoom preview.

In my view, a plugin should be easy to work with. I don’t like plugins that ask to do so many things, like install dependencies like jQuery, jQuery UI, bootstrap and what not. Also, I don’t want a plugin to import so many things like icon library, main stylesheet, theme stylesheet, distribution JavaScript file, etc. Nowadays, isomorphic plugins are in trend. Which means, they can be directly imported in script tag or imported on backend using require or import syntax. This way, people can make use of task runners or bundlers like grunt , gulp or webpack .

In 2018 (in my opinion), a plugin should be a UMD npm module exposing only one object to work with.

The biggest problem is not “How people will consume my plugin?”, it is “How I am going to make it develop it in the first place?”.

A plugin development should be easy and code should be maintainable. It should be divided into different pieces so that other developers and maintainers can find your plugin easy to work with. This is where ES6, SASS, and Webpack comes into the picture.

ES6+ or ES2015+ is how we should write our JavaScript in 2018, for fun and for rapid development. SASS follows the same rule. But since neither ES6 syntax nor SASS is supported in browsers (some support ES6), we need to compile it to ES5 and CSS. This is where Webpack comes in handy.

Webpack is a module bundler. We feed Webpack a single point of entry which is a JavaScript file and Webpack crawls the dependency tree (whatever files imported in the file) and bundles them into a single file. Webpack is also good at doing other sorts of things like compiling SASS into CSS and creating JavaScript code which will dynamically inject CSS into HTML, that means the client doesn’t need to import .css file separately in order for the plugin to work properly.

In this article, we are going to talk about the development process of a simple UserList plugin which simply takes an array of users and injects them in a div. This plugin can dynamically append new users with .addUser method.

Let’s begin by creating a project folder. I will name it user-list and it will be also a package name of the npm module. If you own a git repository, you can clone it or initialize git into this folder. Since you want to make this plugin open source, this is a crucial step.

Next thing is npm init , which means create a package.json file to store dependencies and publish the module to the npm repository.

At this point, you need to understand what packages you need to install to begin development. As we talked about ES6 +SASS, you need a compiler to compile them to ES5 and CSS. You also need a build tool to minify JavaScript and CSS and bundle them into a single JavaScript file. There are many tools you can use like Grunt , Gulp or Webpack . I prefer Webpack, you may choose something else.

To install Webpack, you need to execute the following command

npm install --save-dev webpack webpack-dev-server

Here, two things you should remember. --save-dev saves these packages to devDependencies of package.json , which means these are development packages and the user may avoid installing these package and still use your plugin. webpack-dev-server is a localhost server that you will use to preview how your plugin looks while development. This server will automatically send reload events to the browser to refresh the pages as soon as you make changes to the code so that you don’t have to reload the page manually to see changes.

Webpack doesn’t compile anything on its own. It simply bundles many things together. To compile things like ES6 to ES5 and SASS to CSS, we need loaders.

To load ES6 modules in node.js using import syntax (node.js ATM does not support import syntax), we need to install babel-core and babel-loader .

using syntax (node.js ATM does not support syntax), we need to install and . Babel needs extra presets like babel-preset-env which will do an actual compilation of ES6+ to ES5. If you were using React.js as well, you would also install babel-preset-react to compile JSX to ES5.

which will do an actual compilation of ES6+ to ES5. If you were using as well, you would also install to compile JSX to ES5. babel-preset-transform-object-rest-spread is a plugin to support Object rest/spread syntax if your babel preset doesn’t support it.

is a plugin to support syntax if your babel preset doesn’t support it. To load .scss files into JavaScript file using ES6 import syntax and compile them to CSS, we need to install sass-loader . You will also need node-sass which will do the actual compilation.

files into JavaScript file using ES6 syntax and compile them to CSS, we need to install . You will also need which will do the actual compilation. To minify (for size reduction) and add prefixes to CSS (for browsers compatibility), you need to use postcss-loader . postcss also depends on other plugins like cssnano and autoprefixer to do the actual work.

. also depends on other plugins like and to do the actual work. You need css-loader and style-loader to import .css file into JavaScript (which sass-loader generates) and finally, inject into DOM in style tag.

and to import file into JavaScript (which generates) and finally, inject into DOM in tag. uglifyjs-webpack-plugin is used to minify (compress) JavaScript bundle made by Webpack.

is used to minify (compress) JavaScript bundle made by Webpack. html-webpack-plugin is needed by webpack-dev-server to launch a web preview of your plugin and inject distribution .js files in it.

So, finally, our package.json looks like this.

"name": "user-list",

"version": "1.0.0",

"main": "dist/index.js",

"devDependencies": {

"autoprefixer": "^8.5.0",

"babel-core": "^6.26.3",

"babel-loader": "^7.1.4",

"babel-plugin-transform-object-rest-spread": "^6.26.0",

"babel-preset-env": "^1.7.0",

"css-loader": "^0.28.11",

"cssnano": "^3.10.0",

"html-webpack-plugin": "^3.2.0",

"node-sass": "^4.9.0",

"postcss-loader": "^2.1.5",

"sass-loader": "^7.0.1",

"style-loader": "^0.21.0",

"uglifyjs-webpack-plugin": "^1.2.5",

"webpack": "^3.12.0",

"webpack-dev-server": "^2.11.2"

}

Since we are making a VanillaJS plugin, we do not depend on any other dependencies like React or jQuery. Hence, you only see devDependencies in package.json . main field indicates the file that the user will be importing when he/she uses import UserList from 'user-list' syntax.

Let’s come to the project structure. You want to separate your source folder from the distribution folder. dist or build is normally your distribution folder while src is your source folder.

This is how our directory structure should look like.

user-list

├── dist

| ├── index.html

| └── index.js

├── index.html

├── .babelrc

├── package.json

├── postcss.config.js

├── src

| ├── index.js

| ├── lib

| | ├── user-list.js

| ├── scss

| | └── styles.scss

| └── util

| ├── string.js

└── webpack.config.js

For now, just ignore the content in dist folder because it will be generated by Webpack and we will talk about it later.

Have a look at src folder where our source files live. index.js is the entry point for Webpack, where we will import all the necessary things and export something for the end-user to consume. We have src/lib for feature files, src/util for utility functions and src/scss for .scss files.

Webpack doesn’t care about any of these files besides index.js in the src directory. index.js is an entry file for Webpack and this is because we configured Webpack that way.

webpack.config.js is where we write the configuration for Webpack. This file tells the webpack where the entry file is located and where to output the final distribution file(s).

const path = require('path');

const webpack = require('webpack');

const HTMLWebpackPlugin = require('html-webpack-plugin');

const uglifyJsPlugin = require('uglifyjs-webpack-plugin'); module.exports = {

entry: './src/index.js',

output: {

library: 'UserList',

libraryTarget: 'umd',

libraryExport: 'default',

path: path.resolve(__dirname, 'dist'),

filename: 'index.js'

},

module: {

rules: [

{

test: /\.js$/,

exclude: /node_modules/,

use: ['babel-loader']

},

{

test: /\.scss$/,

use: [

'style-loader',

'css-loader',

'postcss-loader',

'sass-loader'

]

}

]

},

plugins: [

new uglifyJsPlugin(),

new HTMLWebpackPlugin({

template: path.resolve(__dirname, 'index.html')

}),

new webpack.HotModuleReplacementPlugin(),

]

};

To know about Webpack configuration, visit https://webpack.js.org. We are using uglifyJsPlugin to minify JavaScript but you will find your code hard to debug when it’s minified, hence turn it off in development or use sourcemaps.

The crucial thing for our plugin to work is library , libraryTarget and libraryExport keys in output object.

libraryTarget key specifies how a user will import this plugin, using ES6 import syntax or require.js require syntax or something else. We want a user to implement this plugin however he/she wants, hence we used umd which stands for universal module definition.

libraryExport:'default’ will make use of export default syntax to get value to export as a library from src/index.js . This VALUE will be returned when the user uses import VALUE from 'user-list' syntax.

Finally library key specifies variable name which will be available in the browser which will point to this VALUE, which means window.UserList === UserList === export default VALUE .

postcss.config.js file tells postcss-loader to how to process CSS code.

module.exports = {

plugins: [

require('autoprefixer'),

require('cssnano'),

]

};

In Webpack, plugins are invoked in the opposite direction. First, CSS will be minified using cssnano and then prefixed using autoprefixer .

.babelrc file tells babel-loader how to process JavaScript (whichever ES version) files.

{

"presets": [

"env"

],

"plugins": [

"transform-object-rest-spread"

]

}

Let’s see how our index.js source files look like.

// import `.scss` files

import './scss/styles.scss'; // import UserList class

import { UserList as defaultExport } from './lib/user-list'; // export default UserList class

// I used `defaultExport` to state that variable name doesn't matter

export default defaultExport;

import 'file.scss' syntax confuses many people are we are trying to import a SASS file in JavaScript and that’s not exactly legal but Webpack uses sass-loader in combination with other loaders we just installed to make that work.

Then we imported UserList class from src/lib/user-list.js which will provide API of our plugin. Then we just exported that as a plugin. From this moment, our plugin is nothing but UserList class. So, whenever user uses import x from 'user-list' , they are getting UserList class in x . When uses this plugin in the browser, they get UserList in window context (defined in webpack.config.js ).

Next thing is writing UserList class and exporting it from user-list.js file.

// import dependencies

import { concat } from '../util/string'; // return UserList class

export class UserList{

constructor(elem, users){

this.elem = elem;

this.users = users; this.initialized = false;

} // initialize plugin

init() {

let ul = document.createElement( 'ul' );

ul.classList.add('users-list'); // store element reference

this.ul = this.elem.appendChild( ul ); // render initial list of users

this.renderList(); // set initialized to `true`

this.initialized = true;

} // get fullname of the user

getUserFullName( user ) {

return concat( user.firstname, user.lastname );

} // get list of users with fullname

getUsers() {

return this.users.map(

user => this.getUserFullName( user )

);

} // return `li` element with user fullname

getUserLi( fullname ) {

let li = document.createElement( 'li' );

li.innerText = fullname; return li;

} // append `li` element to the users `ul` element

appendLi( li ) {

this.ul.appendChild( li );

} // render entire users list

renderList() {

let users = this.getUsers();

let liElements = users.map(

fullname => this.getUserLi( fullname )

); for( let li of liElements ){

this.appendLi( li );

}

} // add new user

addUser( user ) {

let fullname = this.getUserFullName( user );

let li = this.getUserLi( fullname ); this.appendLi( li );

}

}

In the above file, we have also imported string.js file from util folder which exports concat function.

// export `concat` function which joins strings by space

export function concat(...strings){

return strings.join(' ');

}

And finally our styles.scss SASS file to provide some visual aid.

// plugin level css

.users-list{

width: 400px;

min-height: 200px;

border-radius: 3px;

background-color: #fff;

padding: 15px 30px;

list-style-type: none; >li{

line-height: 30px;

}

}

With these files, we are good to go. Now, the next step is to use Webpack to compile this code into the code that browsers can understand.

Inside your package.json , you will need below commands.

"scripts": {

"build": "webpack",

"start": "webpack-dev-server --open"

},

npm run build will call Webpack to find webpack.config.js and do the work according to the tasks mentioned in the file. npm run start will start Webpack development server which will also use webpack.config.js and open index.html file to show preview.

--open flag will open browser tab automatically.

Let’s talk about index.html file and why it’s there. While a plugin development, you need to test if your plugin is working properly and find out issues if something is not working. This is the file that does the job. You just need to write a simple HTML boilerplate and test your plugin.

The first question that comes to your mind is, where is UserList constructor. Don’t worry, it contains inside dist/index.js because that’s where Webpack has bundled your plugin and HTMLWebpackPlugin injected that into index.html on the fly. You won’t be able to see these changes in dist folder until you run npm run build because webpack-dev-server runs in the memory.

When you run npm run build , Webpack looks at src/index.js (because webpack.config.js has this file as an entry point) and processes different file imports and code in them according to this configuration file. Finally, when everything is done, it outputs index.js into dist folder along with index.html file.

Once you are satisfied with the plugin, you need to make it production-ready. That means you need to put final output files inside dist folder because that’s where main of the package.json is pointing.

We have npm run build task for that. Not only it creates index.js distribution file but also index.html output file which contains index.js inside script tag. You can use live-server with the command live-server dist to launch a localhost server to test if your plugin is working properly after all.