How to create library for Angular(2+) and publish to npm from scratch.
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?
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.jslet indexjscannotseeme = "great!";
let plusTwoNumber= (a,b)=>{
return a+b;
}
module.exports = plusTwoNumber;//index.js
const 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.jsexport default const plusTwoNumber= (a,b)=>{
return a+b;
}//index.js
import 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.
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';
//or
import 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 code
empty-text.directive.ts
empty-text.module.ts
empty-text.service.ts
empty-text.ts/tools — node files for utility work (I’ll explain later)
cleanup.js
copy-package.js
removecode.js/dist — your module that user will get when ‘npm install’package.json
rollup.config.esm.js
rollup.config.umd.js
tsconfig.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 fromimport
) which there’re 2 ways,Node or Classic. for more detail.emitDecoratorMetadata
andexperimentalDecorators
: 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 withtsc
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 youriife
/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
andMISSING_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 needrollup-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 inangular
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 //umd
rollup -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.json
file 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 ofpackage.json
to point to the ES5 UMD bundle.Set the value of the
module
property to point to the entry file of theesm
version of the library.module
is a field inpackage.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 usejsnext:main
instead ofmodule
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.ts
empty-text.directive.d.ts
empty-text.esm.js
empty-text.esm.js.map
empty-text.module.d.ts
empty-text.service.d.ts
empty-text.umd.js
empty-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 tools
folders. 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 withpackage.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.
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMI family. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!