paint-brush
How to create library in Angular 2 and publish to npm from scratch.by@Elec_crafter
48,062 reads
48,062 reads

How to create library in Angular 2 and publish to npm from scratch.

by ElecMay 8th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

<strong>I have an update for make library to compatible with AOT. you can look at the update in “<em>Make your library compatible with AOT section”</em></strong>

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - How to create library in Angular 2 and publish to npm from scratch.
Elec HackerNoon profile picture

http://streetwill.co/posts/240-on-the-table

I have an update for make library to compatible with AOT. you can look at the update in “Make your library compatible with AOT section”

In this post I create library for angular 2 but you can apply this approach to any framework with change a little bit.

If you have been working in front-end field for a while, you will find one day you have to do same work as you did before. I bet you (used to) copy your old code and paste to new project (or you write it from scratch again because your old code is too mess). Don’t worry, almost everyone have done this before including me.

One day I created table ui(with pagination feature) for one project. But in another project also has table ui. I had to copy and paste it to new project. The problem is when I found bug in my table,I have to fix all table component in every project that has the same table. That’s terrible and so boring. There’re many problem with copy and paste which I’m sure you already know it.

So one day I think why don’t I make one table and publish it to npm? It’s really great because I just need to change my code in only one place and just update my table library in every project (with npm update mylovelytable ) and I can install it easily with npm install mylovelytable or change table library code with no fear it will break your old project because it doesn’t update automatically unless you update it by running npm update mylovelytable .

The only one (and big) problem is I didn’t know how to do it :/

So this post is from what I’ve learned creating library in angular from reading many article, look other library code(eg. @angular/material ) try and error.

Thanks to Olivier Combe,Minko Gechev for great posts. I’ve learn a lot from them about creating library in Angular2.

If you understand module system in javascript already,you can skip to Let’s write library code section.

I’ll cover some basic topic. Almost every library is module so I’ll explain module system in javascript first and then I’ll show you the example how I make library in Angular2.

I’m not a professional bad ass programer so this post may be it has mistake so if you guys found some error or improvment. Please comment below,I’ll buy you a cookie :D

Here’s complete souce code for this post, https://github.com/Elecweb/emptytext

Done for shit. Let’s started!

What’s module in javascript?

https://unsplash.com/search/photos/lego?photo=jrW3-eLhWAw

Whatever library you gonna make,module is fundamental. If you’re front-end developer that start from writing jquery,css,html from good old days. When you try to include some library, you download their source code or npm install.

and then you just write script tag to include library file.

<script src=”path/to/jquery.js”></script>

or css

<link rel=”stylesheet” type=”text/css” href=”path/to/theme.css”>

If you include one or two that’s not a big problem, but how about 10 or 20? (These days library is small,self-contained,do one specific job so it can happen). Would it be easier if we can include library we want in javascript file and write script tag only one place? You can even include css in javascript file! So no more dozens of tag script or link.

There’re also many problem from I mention above ,for example:

  • Order dependent. Can you remember when there’s error tell you that there’s no jquery from other library? even you include it already and then you realize you need to include them before some library.
  • Name collision. Declaring every variable in global scope isn’t a good idea. you can never know if you have rewrite declared variable so your application can break easily.
  • Maintainablity and Reusability. It hard to maintain and reuse because without module you may need to write code for many job in single place (or 2 -3) . you never know if you remove or add some code will break existing feature.
  • Portable. Trust me, copy your node_module folder to another sever is really hurt your feeling. You can solve this npm install with package.json (with dependencies version compatible with your project) or copy 4–5 files produced by module bunder.
  • You need to specify path. it may not a big deal but would it be better if you don’t have to ?

Module can help you solve these problem.

How module looks like in javascript?

Module isn’t something new in computer science. There’s module for a long time in other language. Javascript didn’t include it unfortunately.

Whatever programming language is. Module is self-contained code that do one specific job and do it very well. You can include them, remove them or change them with more confident because variable or function isn’ t in global scope and you can explicitly export or hide these variable or function .

You may be used to write everything in one particular file or many files but varaible or function can be accessed across over files. That’s bad you may messed up variable fromscript1.js (like change value) without knowing and your project just break without any compiler error. All you can do is scratch your head and blame the world why it’s so cruel.

When application grow up, your code is mess because it does many job. When your client want you to change your old code or user find some bug, it’s nightmare for you. It make some(or a lot of) time you can figure out your code what they do. (If you never found this problem, trust me it’s really really nightmare).

Let’s look how module works in javascript. I’ll explain Module Format first.

Module Format in Javascript

Module format is syntax we can use to define module.

Before ES2015, JavaScript did not have any official syntax to define modules so smart developer try to define syntax for module

There’re many module format in javascript. For example:

  • Commonjs
  • AMD
  • UMD
  • ES2015

They are just format. It does the same job,make your code be modular.

I’ll show you example of Commonjs.

//idoaboutmathcalculate.js





let indexjscannotseeme = "great!";let plusTwoNumber= (a,b)=>{return a+b;}module.exports = plusTwoNumber;



//index.jsconst plus2num = require('./idoaboutmathcalculate');console.log(plus2num(2,5)); //7

In the above example, there’re 2 filles. idoaboutmathcalculate.js and index.js. As you can see, you can include js file from the other js.

You can choose what varaible you will hide or expose to external. index.js can see only plusTwoNumber function because idoaboutmathcalculate.js expose it only.

You can divide up you code to many file and include,remove and change easily.

For ES2015,it use import and export. For Example:

//idoaboutmathcalculate.js



export default const plusTwoNumber= (a,b)=>{return a+b;}



//index.jsimport plus2num from './idoaboutmathcalculate';console.log(plus2num(2,5)); //7

I’ll not cover all of module format. There’re many post about them that explain it very well.


JavaScript Modules: A Beginner’s Guide_If you’re a newcomer to JavaScript, jargon like “module bundlers vs. module loaders,” “Webpack vs. Browserify” and “AMD…_medium.freecodecamp.com


A 10 minute primer to JavaScript modules, module formats, module loaders and module bundlers_Modern JavaScript development can be overwhelming. When working on a project, you may wonder why all the modern…_www.jvandemo.com

I suggest you to stick with ES2015 because it’s module standard(but not support yet for now).

Whatever module format is. the same purpose of them is just make module happen in Javascript.

But there’re still more to make module works.

What’s Module loader ,Module bundler and Module Resolution?

You have module format but you still can’t use module . Why? Unfortunately browser nowadays doesn’t support it yet but it will!

We need two more things to make it works, module bundler(or Module loader) and module resolution.

I’ll cover detail on module loader and module bundler first.

Module loader

What it does? It load your entry file and dependency on another file.

I’ll show example of using SystemJS which is one of moduler loaders.

app.js

SystemJS.import('./main.js').then((m)=>{

console.log(m.test);

});

main.js




var a = require(‘./sub.js’);module.exports = {test:a};

sub.js

module.exports = "Hello world";

SystemJS will load all the files for you in correct order(app.js → main.js →sub.js). It’s will show “Hello world” in the console.

Module bundler

It will bundle your entry file and and dependency on another file(module) by building a dependency graph and use the graph to generate an optimized bundle where scripts will be loaded in the correct order. For short, when you divide your code to multiple files, module bundler will bundle your multiple files to just only one file for us.

I’ll show example of using module bundler(with Rollup) in Let’s set config for bundler section.

Module bundler and Module loader do similar job but not the same— they load module files to the browser.

The difference is Module loader works in run time and Module bundler work in compile time. Module loader load each file at a time while Module bundler will bundle all module files in compile time to single file and browser will has to load only the bundled file.

Example of module loader are:

RequireJS :loader for modules in AMD format.

SystemJs :loader for modules in AMD,CommonJS, UMD.

Example of module bundler are:

Browserify :bumdler for CommonJS module

Webpack :bundler for AMD,CommonJS,ES6 modules

Rollup.js :bundler for AMD,UMD,CommonJS,ES6 modules

So what should I use ? module bundler or module loader? Well, it depends on you and situation. But in this post I’ll use module bundler because module bundler will emit only one file so your browser need to request just one file.

Last topic in Module, Module resolution.

Module resolution

Look at the example:

import { jquery } from ‘jquery’;

Module resolution tell module bundler where to look the file that you import. It depends on your setting and module bundler. For example, webpack will look for the file in directories specified in resolve.modules if you don’t provide path. It’s default setting is node_modules folder_._

You can use relative path and absolute path as well.



import plus2num from './idoaboutmathcalculate';//orimport plus2num from 'absolute/path/idoaboutmathcalculate';

You can notice there’s no file extension. The default is .js ,you can set default file extension in module bundler config file.

Let’s write some real code!

Ok, I understand some basic idea about module already but how it’s related to create library?

Whatever library you make, it’s all about module — self-contained funtionality, reusable, do only one job and do it very well. If you can create your module, it’s not hard to push your code to npm or whatever.

Are you still here? good. Let’s finally create our lovely library.

The library I’m gonna make is called “emptytext”. This library include emptytext directive to be used for showing “empty” when there’s no text in element or component , service to change the message(which default is “empty”). First let’s setup project and some config.

Here’s the folder structure.





/lib — your source codeempty-text.directive.tsempty-text.module.tsempty-text.service.tsempty-text.ts




/tools — node files for utility work (I’ll explain later)cleanup.jscopy-package.jsremovecode.js

/dist — your module that user will get when ‘npm install’




package.jsonrollup.config.esm.jsrollup.config.umd.jstsconfig.json

For code in this post,you can found here

First let’s create a package.json file and follow the prompts.

npm init

I will use typescript for writing code in this post (it doesn’t require).

npm install -g typescript

and set some config for typescript compiler. I create config file namedtsconfig.json .













{“compilerOptions”: {“target”: “es5”,“module”: “es2015”,“sourceMap”: true,“moduleResolution”: “node”,“emitDecoratorMetadata”: true,“experimentalDecorators”: true,“declaration”: true,“outDir”: “./dist”,“lib”: [“es2015”, “dom”]},“files”: [“./lib/empty-text.ts”]

}

  • target : javascript(ECMAScript) version of compiled code.
  • module : module format of complied code. There’re several options,for example, UMD,ES2015,AMD.
  • sourcemap: tell typescript compiler to generate sourcemap.
  • module resolution : how modules get resolved(where to look your dependency file from import) which there’re 2 ways,Node or Classic. for more detail.
  • emitDecoratorMetadata and experimentalDecorators : you need to set it true for angular (they use it,for example, @Component and @Directive.
  • declaration : generate corresponding .d.ts file(definition file).
  • lib: built-in API declaration(difinition file) that you can choose to include in your project.
  • files : it’s where your typescript file that you want to compile with tsc

Now I can compile typescript code to javascript code with

tsc -p tsconfig.json // or just tsc

In this library, I need `angular/core`

npm install @angular/core --save

Don’t worry much how to compile our code for now,I’ll explain later.

Let’s write library code

I’ll not explain code much, it’s easy to understand and not the point of this post.

Here’s the directive. lib/empty-text.directive.ts

Here’s the service for setting message, lib/empty-text.service.ts

Here’s the module for user to import. lib/empty-text.directive.ts

Create entry file for export all other files in library, empty-text.ts

export * from "./empty-text.module";

export * from "./empty-text.directive";

export * from "./empty-text.service";

That’s it. This library is small, just 4 files.

Let’s set config for bundler

You need to bundle code in files to a single file which format is UMD(UMD is Universal Module Definition, it can be used with any module format) and ES2015.

The reasom we provide ES2015 because user can get benefit from Tree Shaking if they use Rollup or Webpack.

There’re no document about how you should provide code in library for user in Angular (If anyone know about this,please comment below).

So I look at _@angular/material_ how they build their file and use their format in my library.

First, you need module bundler. It can be Webpack, Rollup or whatever. in this post, I’ll use Rollup.

npm install --global rollup

You need to bundle your code in two format,UMD and ES2015 so there’re two config file for Rollup.

The different is format property which set umd and es for UMD and ES2015 respectively.

Since config file for Rollup is normal javascript file,you can import and change only for format property,so you don’t need to duplicate your other config properties by hand.

I’ll set config file for umd first.

Here’s the config file,rollup.config.umd.js

  • input: where the entry file you want to bundle.
  • output : config for bundled file which have following:
  • output.format : module format of your bundled file.
  • output.name: The variable name, representing your iife/umd bundle.
  • output.sourcemap : Set to true so Rollup will provide sourcemap.(it’s not necessary in this case because I don’t minify my code for simplicity.)
  • output.file : The file to write to.
  • external : exclude dependency code in bundled code,which in this case, I assume user who use this library have installed @angular/core
  • onwarn : Function that will intercept warning message. I ignore two unnecessary warning( THIS_IS_UNDEFINED and MISSING_GLOBAL_NAME ).
  • plugins : plugin used in Rollup. I’ll explan next what plugin I use and why

We need to use plugins in Rollup for make magic happes.

  • rollup-plugin-node-resolve
  • rollup-plugin-commonjs
  • rollup-plugin-angular
  • rollup-plugin-typescript
  • node-sass

I’ll explain what these plugin do. But first, let’s install these plugins.

npm install --save-dev rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-angular rollup-plugin-typescript node-sass

Let’s look at plugins section

excerpt from rollup.config.umd.js

  • angular : If you don’t inline template and style for component, you need rollup-plugin-angular to parse external template and style to be included in component.
  • node-sass : Parse sass to css before styles are included in component. we use it in angular plugin

if(scss){

css = sass.renderSync({ data: scss }).css.toString();

}else{

css = '';

}

return css;

The Library in the example doesn’t have any component but I want to show how you can config for library that provide component. If your library doesn’t provide component, you can skip rollup-plugin-angular and node-sass config section.

  • typescript : Transpling Typescript to plain javascript.
  • resolve : Locate modules using the Node resolution algorithm.
  • commonjs : Rollup can understand only module format ES2015 but some library you may install are commonjs so you need this plugin so that Rollup can understand commonjs module.

For ES2015 module config file,we don’t have to duplicate by hand. just import config from rollup.config.umd.js and overide some config.

import config from './rollup.config.umd.js';

import {nameLibrary,PATH_DIST} from './config-library.js'


config.output.format = "es";config.output.file = PATH_DIST+nameLibrary+".esm.js"

export default config;

I override output.format to “es” and output.file for give name of bundled file.

You may have noticed, I import some variable from config-library.js for variables defined for name of bundled file and destination path so that we don’t have to duplicate it.

config-library.js

export const nameLibrary = "empty-text";

export const PATH_SRC = "lib/";

export const PATH_DIST = "dist/";

Now you can bundle with defined config with command:


rollup -c rollup.config.umd.js //umdrollup -c rollup.config.esm.js //es2015

Get rid of unnecessary files for user .

There’re unneccessary files when we bundle like .ts file (because we will provide one bundle javascript file per module format)

I’ll use node to handle deleting unnessary files and copy and paste package.jsonfile to dist folder for us (I’ll explain why soon).

In tools folder, I create copy-package.js for copy package.json in root folder to dist folder.

const fs = require('fs');

let resizable = fs.readFileSync('package.json').toString();

fs.writeFileSync('dist/package.json', resizable);

and cleanup-package.js which for deleting script section and devDependencies .

const fs = require('fs');

const packageJson = JSON.parse(fs.readFileSync('./dist/package.json').toString());

delete packageJson.devDependencies;

delete packageJson.scripts;

fs.writeFileSync('./dist/package.json', JSON.stringify(packageJson, null, 2));

The last file is removecode.js for remove unnecessary javscript file emitted from Typescript compiler. We just want definition typescript for IDE(autocomplete for example) to provide for user.

const del = require('del');

del(['dist/!(*.umd.js|*.esm.js|*.d.ts|*.umd.js.map|*.esm.js.map)']).then(paths => {

console.log('Files and folders that would be deleted:\\n', paths.join('\\n'));

});

It remove every file except for our bundled files( *.umd.js , *.esm.js), definition typscript ( *.d.ts) and sourcemap ( *.umd.js.map, *.esm.js.map).

I use del library for remove files .

Install del from npm.

npm install --save-dev del

Now you can run script we defined in this section with node command.For example, if you want to copy and paste package.json to dist folder, just run node tools/copy-package.js in your favourite command line.

There’re many command we need to run. 1) We need to remove dist folder for clearing files. 2) bundle file with rollup (2 commands for umd and ES2015. 3) run node scripts( 3 commands for copy package.json, clear script and devdependencies in package.json, remove unnessary code).

It’d be better if we just run one command that do these tasks for us.

Set npm script for easily build

Finally you can build the library ! the last task is set script in package.json so you don’t have to type many command to build our code.

...

"scripts": {

"copy": "node tools/copy-package.js && node tools/cleanup.js",

"bundle": "rimraf dist && rollup -c rollup.config.umd.js && rollup -c rollup.config.esm.js && tsc",

"postbundle": "node tools/removecode.js",

"build": "npm run bundle && npm run copy"

},

Now you need to just write one command

npm run build

Set some config for module resolution

When user import our library folder, it’ll look for the files where we provide in properties ofpackage.json . For more detail — in Distributing the components section

Set the value of the _main_ property of _package.json_ to point to the ES5 UMD bundle.

Set the value of the _module_ property to point to the entry file of the _esm_ version of the library. _module_is a field in _package.json_ where bundlers such as rollup and webpack 2 expect to find a reference to the ES2015 version of the code. Note that some older versions of the bundlers use _jsnext:main_instead of _module_ so we need to set both properties.

package.json

"main": "empty-text.umd.js",

"jsnext:main": "empty-text.esm.js",

"module": "empty-text.esm.js",

"types": "empty-text.d.ts",

Make your library compatible with AOT [Updated!(26/07/2017)]

Since library for angular 2 need to be compatible with AOT. You need to run ngc to emit .metadata.json along side to your package.

First install @angular/compiler-cli :

npm install --save-dev @angular/compiler-cli

and then update package.json in scripts section:


"scripts": {...,


"bundle": "rimraf dist && rollup -c rollup.config.umd.js && rollup -c rollup.config.esm.js && tsc && ngc", //<--- add ngc...

},

Last thing is update in tools/removecode.js for not removing .metadata.json files which emited from ngc command

del(['dist/!(*.umd.js|*.esm.js|*.d.ts|*.umd.js.map|*.esm.js.map|*.metadata.json)']).then(paths => {

console.log('Files and folders that would be deleted:\\n', paths.join('\\n'));

});

Let’s build and publish to the world~

You can now build file with command npm run build which will run bundle and copy scripts. Notice postbundle ,it’ll be run after bundle complete.

After run build script,we will get files in dist folder








empty-text.d.tsempty-text.directive.d.tsempty-text.esm.jsempty-text.esm.js.mapempty-text.module.d.tsempty-text.service.d.tsempty-text.umd.jsempty-text.umd.js.map

Only one task left is publishing it to npm.

We can publish library code to npm with just command npm run publish .

But wait! if I publish my library for now, user will get unnessesary files like our lib and toolsfolders. We should get rid of them.

There’re 2 ways to do this.

  • create .npmignore to ignore unnessary files for publishing.
  • publish in dist folder with package.json .

I’ll use the second way in this post. That’s why I need to copy package.json to dist folder.

We don’t need to copy and paste by hand because copy-package.js will do the work for you.

Navigate to dist folder and npm publish from there. So user that install our library will get files only from dist folder.

PS. Navigate dist folder and run command from there is boring but I can’t find the way to publish from dist folder in root folder. If you have some suggestion,please let me know in the comment.

UPDATE

Maxime Lafarie suggest me to include “publish”: “cd ./dist && npm publish” in script section. So we just need to run npm run publish .

Phew.. It’s a long road. You’re really good reader. Now you can publish your library to the world!.

User just npm install emptytext and import to their module like this.

import { EmptyTextModule } from ‘emptytext’;

People now can use sweet directive (or you in the future.Trust me you will thanks yourself).

If you have any question, or some suggestion(really applicated!).Please comment below.

It’s really long post. Take a break and have coffee party.