The complete Angular Library starter.
- Minimal configuration. tsconfig, AOT, webpack config are all automated.
- Optional Scoping (
npm install @my-scope/my-lib
) - Multiple library repository (
@my-scope/my-lib
,@my-scope/her-lib
, ... all in the same repo) - Package extensions.
Ship packages with an opt-in internal package (e.g:
@angular/core/testing
), extension are built separately. - Build hooks, tap in to alter package.json, tsconfig, rollup config etc... (globally and per package)
- Webpack powered.
- Outputs:
- Flatten ES Modules (es5, es2015)
- Rollup UMD bundle + minified version + gzipped version
- Source maps, complete and accurate.
- Flat Angular compiler modules - 1
metadata.json
file. @angular
grade npm packages, tree shake compatible.- Resource inlining (html, css, scss) for source code + metadata.json
- Resource inlining driven by webpack, no fuzzy tasks, use your favourite webpack loaders.
- Dedicated demo app, write as if your library is a module (no relative imports)
- Simulation mode - Run the demo app on the prod compiled library as if its in
node_modules
, in dev or prod mode. - E2E + Simulation - Run E2E in simulation mode
- JEST unit testing
Make sure you have Node version >= 6.0 and NPM >= 3
Clone the repo using same instruction as in AngularClass/angular-starter
# clone the repo
# --depth 1 removes all but one .git commit history
git clone --depth 1 https://github.com/shlomiassaf/angular-library-starter.git
# change directory to the repo
cd angular-library-starter
# WINDOWS only. In terminal as administrator
npm install -g node-pre-gyp
# install the repo with npm
npm install
# You can also use yarn
# if you're in China use cnpm
# https://github.com/cnpm/cnpm
The repo comes with a demo app and a simple library call my-lib
Fire up the dev server with both demo app and library bundled in dev mode - JIT.
npm run start
go to http://0.0.0.0:3000 or http://localhost:3000 in your browser
You can now make changes to the demo app or the libraries and see it live in your browser.
Go to Examples
The
DIST_OUTPUT
folder shows the output of the compilation.
NPM scripts as defined in angular-starter did not change. The added Library commands are all prefixed with lib:
For example,
npm run test
will fire up karma and run unit tests for the demo app.npm run lib:test
will fire up jest and run unit tests for libraries only.
Sync configuration files. This command align configuration files with the library configuration. Most of the sync concerns path mapping for typescript and jest.
Run this command after each change to a package configuration that results in a file structure change. This includes adding, removing or changing a package name, Changing the top-level scope, etc..
The sync process rewrites the paths
mapping in tsconfig.json
, you
can use hooks to tap into this process and add custom path mappings.
This will make sure that your mappings are re-generated on each rewrite.
Fires up the build process (release) for all libraries.
To run build for a specific library: npm run lib:build -- --select my-lib
Build output for library is in the root folder under dist_package
folder.**
Fires up the library build process and once done fires an AOT production
build to the demo app in simulation mode
Simulation mode: The demo app will consume the compiled library and not
the source code, having an AOT bundle exactly the same to having the library
in node_modules
. Completely native, no npm link or hard copy to node_modules
.
This command is equivilant to running
lib:build
and thennpm run build:aot:prod -- --env.sim
Run unit tests (JEST) for all libraries.
Run unit tests (JEST) in watch mode for all libraries.
Fires up the library build process and once done fires the dev server with the demo app in simulation mode
This is similar to lib:build:sim but the demo application is running in dev mode with JIT compiler. The library however is AOT compiled so it is a good way to test your library, after compilation, working with JIT.
The scripts, combined with the original angular-starter scripts can compose a full CI process:
For example, consider the following workflow:
lib:build:sim
- run an AOT build for both lib and demo appnpm run lib:test
- run library unit testsnpm run test
- run demo app integration tests (if applicable)npm run e2e
- run E2E tests
If all passes, your library is good to publish.
E2E tests run on the demo app, make sure to focus your tests on the library and it's UI components, less on the demo app.
The root folder for all source files (demo app + libraries) is /src
The demo application is fixed to the directory demo
All other directories under src
are libraries, the name of each folder
must correspond to the value/s in the global library configuration packages
property.
For scoped packages, all packages are inside a directory named after the scope and the scope directory is in
src
.
For the following configuration:
"libConfig": {
"packages": [
"my-lib",
"my-lib-N"
]
}
This should be the file structure:
angular-library-starter/
├*─src/ * Root folder for all source files
│ ├*─demo/ * demo app root.
│ │ ├──index.html * Index.html: where we generate our index page
│ │ ├──main.browser.ts * our entry file for our browser environmen
│ │ ├──app/ * WebApp: folder
│ │ │ └──... ╔═══════════════════════════════════╗
│ │ │ ║ ║
│ │ ├──styles/ ║ See the angular-starter reop for ║
│ │ │ ║ details information ║
│ │ ├──assets/ ║ ║
│ │ └──... ╚═══════════════════════════════════╝
│ │
│ ├──my-lib/ * Library root folder
│ │ ├*─src/ * Library source code
│ │ │ ├──... * Library content
│ │ │ ├──module.ts * NgModule declration module
│ │ │ └──index.ts * Library entry files (configurable)
│ │ │
│ │ ├──test/ * Unit tests
│ │ │ └──...spec.ts
│ │ │
│ │ ├──build_hooks.ts * Local script to intercept and interact with the build process
│ │ └──package.json * Local configuration and manifest data
│ │
│ │
│ ├──my-lib-N/ ╔═══════════════════════════════════╗
│ │ ├*─src/ ║ You can manage mutiple ║
│ │ │ ├──... ║ libraries in the same repo ║
│ │ │ ├──module.ts ║ and/or under the same scope ║
│ │ │ └──index.ts ║ ║
│ │ ├──build_hooks.ts ║ ║
│ │ └──package.json ╚═══════════════════════════════════╝
Some notes:
-
This is a fairly simple setup. For Scope and extensions scroll down.
-
Unit tests are in the
test
folder, sibling tosrc
. This is recommendation. -
From the above it comes clear that you can't name a package
demo
(unless it is scoped) I believe it should be ok. If however you feel the urge to do so you can start following the configuration files and changing the demo directory into something of your choice. -
This section focused on the file structure for the code, for a complete structure review for the whole repo see AngularClass/angular-starter
Here is the output of a single library setup, identical to to above but with only one library. (my-lib-N is omitted)
There are 2 locations for configuration:
Defines the scope (optional) and libraries in this repo.
export interface GlobalLibConfig {
scope?: string;
packages: string[]
}
Global configuration is set in the package.json
file at the root of this repo
"libConfig": {
"packages": [
"my-lib"
]
}
Defines the entry point (optional), resource inline and extension libraries for a library..
export interface LocalLibConfig {
/**
* The entry file name, without extension.
* defaults to 'index' if not set.
*
* Use in multi-library configuration to avoid AOT compilation hell with 'index' addition
*/
entry?: string;
/**
* Internal extensions for the library.
* If a library declares a "libExtensions" property in it's internal package.json file
* it is considered as instructions for creating internal umd bundles.
*
* THIS OBJECT IS SUBJECT TO CHANGE.
* Most probably it will be an array of string where metadata for each extension
* will be set inside it's package.json and not in the parent.
*/
libExtensions?: Array<LibraryExtension>;
}
Local configuration is set in the package.json
file at the root of each library
{
"version": "1.0.0",
"description": "Sample Library",
"keywords": [ ],
"libConfig": {
"inlineResources": true
}
}
In npm scope is a simple namespace, here is an example to apply the scope @king:
"libConfig": {
"scope": "@king",
"packages": [
"my-lib",
"my-lib-N"
]
}
This should be the file structure:
angular-library-starter/
├*─src/
│ ├*─demo/
│ │ └──...
│ └──@king
│ ├─my-lib/
│ │ ├*─src/
│ │ │ ├──...
│ │ │ ├──module.ts
│ │ │ └──index.ts
│ │ ├──build_hooks.ts
│ │ └──package.json
│ │
│ └──my-lib-N/
│ ├*─src/
│ │ ├──...
│ │ ├──module.ts
│ │ └──index.ts
│ ├──build_hooks.ts
│ └──package.json
TODO - most of it is already explained.
A Library extension is a library inside a library. An extension goes through the same build process a parent library does, the only difference is that it is not published as a unique package, it comes with the parent NPM packages and to access it, a deep import URI is required.
For example, the testing frameworks in each @angular
package is an internal
package, an extension. Not part of the angular package bundle but you can opt-in to it
if you want.
import { inject, TestBed } from '@angular/core/testing';
Notice the deep import? this is by design.
The testing
namespace is not part of the @angular/core
bundle even though it might
seems like it is. An internal of @angular/core
will be under @angular/core/src/...
The testing
extension is part of the NPM package but it will not load by default, a specific import URI is required.
The starter adopts this structure.
An extensions is a top-level path inside the library's root directory that is:
- Not the
src
directory (which is the main package) - Listed in the libExtensions config section in the local configuration
Note the terminology, top-level path
, it can be a a single directory
or a path starting from the root of the library.
We are building a library called my-lib and we publish it under the scope @king
my-lib has 1 extensions and 2 plugins:
- testing
- plug-a
- plug-b
We might have a lot of plugins in the future so we want to group all plugins
inside a container directory called plugins
.
Extensions and plugins are similar in structure, they only differ by their logical intent. The testing extensions is a tool to use while unit testing and not to be included in a production build. Plugins are for production, they are opt-in packages that provide functionality to the main package but is not mandatory. This is the terminology I use, you might choose to name it differently.
angular-library-starter/
├*─src/
│ ├*─demo/
│ │ └──...
│ │
│ └─@king * The scope for all libraries
│ │
│ └─my-lib/ * Main library - root
│ │
│ ├*──src/ * Source code - Main library
│ │ ├──...
│ │ ├──module.ts
│ │ └──my-lib-entry.ts
│ │
│ ├───test/ * Unit tests - Main library (src)
│ │ └──...spec.ts
│ │
│ │ ════════════════════════════════════════════════════
│ │
│ ├───testing/ * Testing extension
│ │ │
│ │ ├*──src/ * Source code - Testing extension
│ │ │ ├──...
│ │ │ ├──module.ts
│ │ │ └──index.ts
│ │ │
│ │ ├───test/ * Unit tests - Testing extension
│ │ │ └──...spec.ts
│ │ │
│ │ ├──build_hooks.ts * Hook interaction - Testing extension
│ │ └──package.json
│ │
│ │ ════════════════════════════════════════════════════
│ │
│ ├───plugins/ * Plugins CONTAINER
│ │ │
│ │ ├───plug-a/ * Plug-A extension
│ │ │ │
│ │ │ ├*──src/ * Source code - Plug-A extension
│ │ │ │ ├──...
│ │ │ │ ├──module.ts
│ │ │ │ └──index.ts
│ │ │ │
│ │ │ ├───test/ * Unit tests - Plug-A extension
│ │ │ │ └──...spec.ts
│ │ │ │
│ │ │ ├──build_hooks.ts * Hook interaction - Plug-A extension
│ │ │ └──package.json
│ │ │
│ │ │
│ │ └───plug-b/ * Plug-B extension
│ │ │
│ │ ├*──src/ * Source code - Plug-B extension
│ │ │ ├──...
│ │ │ ├──module.ts
│ │ │ └──index.ts
│ │ │
│ │ ├───test/ * Unit tests - Plug-B extension
│ │ │ └──...spec.ts
│ │ │
│ │ ├──build_hooks.ts * Hook interaction - Plug-B extension
│ │ └──package.json
│ │
│ │ ════════════════════════════════════════════════════
│ │
│ │
│ ├──build_hooks.ts * Hook interaction - Main package
│ └──package.json
package.json
(root)
"libConfig": {
"scope": "@king",
"packages": [ "my-lib" ]
}
src/@king/my-lib/package.json
(library main)
{
"version": "1.0.0",
"description": "Sample Library",
"libConfig": {
"entry": "my-lib-entry",
"libExtensions": [
{
"name": "testing"
},
{
"name": "plug-a",
"dir": "plugins/plug-a"
},
{
"name": "plug-b",
"dir": "plugins/plug-b"
}
]
}
}
src/@king/my-lib/testing/package.json
{
"version": "1.0.0",
"description": "Testing module"
}
src/@king/my-lib/plugins/plug-a/package.json
{
"version": "1.0.0",
"description": "A plugins that is always A"
}
src/@king/my-lib/plugins/plug-b/package.json
{
"version": "1.0.0",
"description": "A plugins that is always B"
}
To access plug-b
from an angular application:
import { ... } from '@king/my-lib/plug-b';
Bundling FESM and UMD modules requires a manual definition of the dependency graph.
This is to prevent duplicate code, so that my-lib
which uses @angular/core
will
not include @angular/core
in it's production bundle.
This is also true for extensions, they are like any other package.
Remember to define the inter-dependency graph for extensions and multi-library configuration and of course 3rd party dependencies.
This is reviewed in-depth in thr Hooks section.
TODO DOCS
- Hooks run using a script, its name is always
build_hooks
- It can be TypeScript or JS file.
- Hooks can be global, local or both
- Global Hooks
- Run BEFORE local hooks
build_hooks
script is in the root of the project
- Local Hooks
- Run AFTER the global hook
build_hooks
script is in the root of the library (sibling to it'ssrc
directory)
Executes before saving the manifest file but after merging the local manifest with the global manifest. Params: package.json object
Executes before running the rollup bundling process for UMD. Params: Limited Rollup config object
Executes before running the rollup bundling process for FESM (es5 and es2015). Params: Limited Rollup config object
Executes before saving tsconfig.json
to disk and executing webpack.
Use this hook to modify tsconfig
before saving it to disk and using it as the compilation configuration.
Params: tsconfig object
lib:sync
invokes the global hook fortsconfig
with the maintsconfig.json
. Since all othertsconfig
files inherit from it, use this global hook to add custom paths and TS configuration.
Executes before saving jest.library.config.json
to disk.
GLOBAL HOOK only.
Params: jest config object
lib:sync
invokes the global hook forjest.library.config
with the mainjest.library.config.json
. This is the place to add custom JEST configuration and additional (custom) path mappings.
module.exports.packageJSON = function(pkgJson) { };
module.exports.jestConfig = function(jestConfig) { };
module.exports.tsconfig = function tsconfig(config) {}
module.exports.rollupFESM = function(rollupConfig) { };
module.exports.rollupUMD = function(rollupConfig) { };
TODO DOCS
- Documentation
- Examples for popular scenarios and complex setups
- Doc system
- Expose release tools to NPM scripts (detect, commit)
- Build more release tools